@jonit-dev/night-watch-cli 1.8.1 → 1.8.3

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, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY, QUEUE_LOCK_FILE_NAME;
35
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,13 +217,15 @@ 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";
198
225
  HISTORY_FILE_NAME = "history.json";
199
226
  PRD_STATES_FILE_NAME = "prd-states.json";
200
227
  STATE_DB_FILE_NAME = "state.db";
228
+ GLOBAL_NOTIFICATIONS_FILE_NAME = "global-notifications.json";
201
229
  MAX_HISTORY_RECORDS_PER_PRD = 10;
202
230
  DEFAULT_QUEUE_ENABLED = true;
203
231
  DEFAULT_QUEUE_MODE = "conservative";
@@ -208,7 +236,8 @@ var init_constants = __esm({
208
236
  reviewer: 40,
209
237
  slicer: 30,
210
238
  qa: 20,
211
- audit: 10
239
+ audit: 10,
240
+ analytics: 10
212
241
  };
213
242
  DEFAULT_QUEUE = {
214
243
  enabled: DEFAULT_QUEUE_ENABLED,
@@ -223,6 +252,15 @@ var init_constants = __esm({
223
252
  }
224
253
  });
225
254
 
255
+ // ../core/dist/board/types.js
256
+ var BOARD_COLUMNS;
257
+ var init_types2 = __esm({
258
+ "../core/dist/board/types.js"() {
259
+ "use strict";
260
+ BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
261
+ }
262
+ });
263
+
226
264
  // ../core/dist/config-normalize.js
227
265
  function validateProvider(value) {
228
266
  const trimmed = value.trim();
@@ -418,6 +456,20 @@ function normalizeConfig(rawConfig) {
418
456
  };
419
457
  normalized.audit = audit;
420
458
  }
459
+ const rawAnalytics = readObject(rawConfig.analytics);
460
+ if (rawAnalytics) {
461
+ const targetColumnRaw = readString(rawAnalytics.targetColumn);
462
+ const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
463
+ const analytics = {
464
+ enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
465
+ schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
466
+ maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
467
+ lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
468
+ targetColumn,
469
+ analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
470
+ };
471
+ normalized.analytics = analytics;
472
+ }
421
473
  const rawJobProviders = readObject(rawConfig.jobProviders);
422
474
  if (rawJobProviders) {
423
475
  const jobProviders = {};
@@ -477,6 +529,7 @@ function normalizeConfig(rawConfig) {
477
529
  var init_config_normalize = __esm({
478
530
  "../core/dist/config-normalize.js"() {
479
531
  "use strict";
532
+ init_types2();
480
533
  init_constants();
481
534
  }
482
535
  });
@@ -700,6 +753,25 @@ function buildEnvOverrideConfig(fileConfig) {
700
753
  if (!isNaN(v) && v > 0)
701
754
  env.audit = { ...auditBase(), maxRuntime: v };
702
755
  }
756
+ const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
757
+ if (process.env.NW_ANALYTICS_ENABLED) {
758
+ const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
759
+ if (v !== null)
760
+ env.analytics = { ...analyticsBase(), enabled: v };
761
+ }
762
+ if (process.env.NW_ANALYTICS_SCHEDULE) {
763
+ env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
764
+ }
765
+ if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
766
+ const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
767
+ if (!isNaN(v) && v > 0)
768
+ env.analytics = { ...analyticsBase(), maxRuntime: v };
769
+ }
770
+ if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
771
+ const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
772
+ if (!isNaN(v) && v > 0)
773
+ env.analytics = { ...analyticsBase(), lookbackDays: v };
774
+ }
703
775
  const jobProvidersEnv = {};
704
776
  for (const jobType of VALID_JOB_TYPES) {
705
777
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -792,6 +864,7 @@ function getDefaultConfig() {
792
864
  claudeModel: DEFAULT_CLAUDE_MODEL,
793
865
  qa: { ...DEFAULT_QA },
794
866
  audit: { ...DEFAULT_AUDIT },
867
+ analytics: { ...DEFAULT_ANALYTICS },
795
868
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
796
869
  queue: { ...DEFAULT_QUEUE }
797
870
  };
@@ -858,7 +931,7 @@ function mergeConfigLayer(base, layer) {
858
931
  ...layerQueue,
859
932
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
860
933
  };
861
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
934
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
862
935
  base[_key] = {
863
936
  ...base[_key],
864
937
  ...value
@@ -943,15 +1016,6 @@ var init_config = __esm({
943
1016
  }
944
1017
  });
945
1018
 
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
1019
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
956
1020
  import Database from "better-sqlite3";
957
1021
  import { inject, injectable } from "tsyringe";
@@ -1966,11 +2030,18 @@ var init_github_projects = __esm({
1966
2030
  await this.ensureStatusColumns(existing.id);
1967
2031
  return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
1968
2032
  }
1969
- const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
1970
- createProjectV2(input: { ownerId: $ownerId, title: $title }) {
1971
- projectV2 { id number url title }
2033
+ const createData = await graphql(`
2034
+ mutation CreateProject($ownerId: ID!, $title: String!) {
2035
+ createProjectV2(input: { ownerId: $ownerId, title: $title }) {
2036
+ projectV2 {
2037
+ id
2038
+ number
2039
+ url
2040
+ title
2041
+ }
2042
+ }
1972
2043
  }
1973
- }`, { ownerId: owner.id, title }, this.cwd);
2044
+ `, { ownerId: owner.id, title }, this.cwd);
1974
2045
  const project = createData.createProjectV2.projectV2;
1975
2046
  this.cachedProjectId = project.id;
1976
2047
  await this.linkProjectToRepository(project.id);
@@ -1986,24 +2057,34 @@ var init_github_projects = __esm({
1986
2057
  const message = err instanceof Error ? err.message : String(err);
1987
2058
  if (!message.includes("Status field not found"))
1988
2059
  throw err;
1989
- const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
1990
- createProjectV2Field(input: {
1991
- projectId: $projectId
1992
- dataType: SINGLE_SELECT
1993
- name: "Status"
1994
- singleSelectOptions: [
1995
- { name: "Draft", color: GRAY, description: "" }
1996
- { name: "Ready", color: BLUE, description: "" }
1997
- { name: "In Progress", color: YELLOW, description: "" }
1998
- { name: "Review", color: ORANGE, description: "" }
1999
- { name: "Done", color: GREEN, description: "" }
2000
- ]
2001
- }) {
2002
- projectV2Field {
2003
- ... on ProjectV2SingleSelectField { id options { id name } }
2060
+ const createFieldData = await graphql(`
2061
+ mutation CreateStatusField($projectId: ID!) {
2062
+ createProjectV2Field(
2063
+ input: {
2064
+ projectId: $projectId
2065
+ dataType: SINGLE_SELECT
2066
+ name: "Status"
2067
+ singleSelectOptions: [
2068
+ { name: "Draft", color: GRAY, description: "" }
2069
+ { name: "Ready", color: BLUE, description: "" }
2070
+ { name: "In Progress", color: YELLOW, description: "" }
2071
+ { name: "Review", color: ORANGE, description: "" }
2072
+ { name: "Done", color: GREEN, description: "" }
2073
+ ]
2074
+ }
2075
+ ) {
2076
+ projectV2Field {
2077
+ ... on ProjectV2SingleSelectField {
2078
+ id
2079
+ options {
2080
+ id
2081
+ name
2082
+ }
2083
+ }
2084
+ }
2004
2085
  }
2005
2086
  }
2006
- }`, { projectId: project.id }, this.cwd);
2087
+ `, { projectId: project.id }, this.cwd);
2007
2088
  const field = createFieldData.createProjectV2Field.projectV2Field;
2008
2089
  this.cachedFieldId = field.id;
2009
2090
  this.cachedOptionIds = new Map(field.options.map((o) => [o.name, o.id]));
@@ -2030,11 +2111,23 @@ var init_github_projects = __esm({
2030
2111
  async createIssue(input) {
2031
2112
  const repo = await this.getRepo();
2032
2113
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2033
- const issueArgs = ["issue", "create", "--title", input.title, "--body", input.body, "--repo", repo];
2114
+ const issueArgs = [
2115
+ "issue",
2116
+ "create",
2117
+ "--title",
2118
+ input.title,
2119
+ "--body",
2120
+ input.body,
2121
+ "--repo",
2122
+ repo
2123
+ ];
2034
2124
  if (input.labels && input.labels.length > 0) {
2035
2125
  issueArgs.push("--label", input.labels.join(","));
2036
2126
  }
2037
- const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, { cwd: this.cwd, encoding: "utf-8" });
2127
+ const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
2128
+ cwd: this.cwd,
2129
+ encoding: "utf-8"
2130
+ });
2038
2131
  const issueUrl = issueUrlRaw.trim();
2039
2132
  const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
2040
2133
  if (!issueNumber)
@@ -2042,11 +2135,15 @@ var init_github_projects = __esm({
2042
2135
  const [owner, repoName] = repo.split("/");
2043
2136
  const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
2044
2137
  const issueJson = { number: issueNumber, id: nodeIdRaw.trim(), url: issueUrl };
2045
- const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2046
- addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2047
- item { id }
2138
+ const addData = await graphql(`
2139
+ mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2140
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2141
+ item {
2142
+ id
2143
+ }
2144
+ }
2048
2145
  }
2049
- }`, { projectId, contentId: issueJson.id }, this.cwd);
2146
+ `, { projectId, contentId: issueJson.id }, this.cwd);
2050
2147
  const itemId = addData.addProjectV2ItemById.item.id;
2051
2148
  const targetColumn = input.column ?? "Draft";
2052
2149
  const optionId = optionIds.get(targetColumn);
@@ -2066,11 +2163,45 @@ var init_github_projects = __esm({
2066
2163
  assignees: []
2067
2164
  };
2068
2165
  }
2166
+ async addIssue(issueNumber, column = "Ready") {
2167
+ const repo = await this.getRepo();
2168
+ const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2169
+ const [owner, repoName] = repo.split("/");
2170
+ const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
2171
+ const nodeId = nodeIdRaw.trim();
2172
+ if (!nodeId)
2173
+ throw new Error(`Issue #${issueNumber} not found in ${repo}.`);
2174
+ const addData = await graphql(`
2175
+ mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2176
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2177
+ item {
2178
+ id
2179
+ }
2180
+ }
2181
+ }
2182
+ `, { projectId, contentId: nodeId }, this.cwd);
2183
+ const itemId = addData.addProjectV2ItemById.item.id;
2184
+ const optionId = optionIds.get(column);
2185
+ if (optionId)
2186
+ await this.setItemStatus(projectId, itemId, fieldId, optionId);
2187
+ const full = await this.getIssue(issueNumber);
2188
+ if (full)
2189
+ return { ...full, column };
2190
+ throw new Error(`Added issue #${issueNumber} to project but failed to fetch it back.`);
2191
+ }
2069
2192
  async getIssue(issueNumber) {
2070
2193
  const repo = await this.getRepo();
2071
2194
  let rawIssue;
2072
2195
  try {
2073
- const { stdout: output } = await execFileAsync2("gh", ["issue", "view", String(issueNumber), "--repo", repo, "--json", "number,title,body,url,id,labels,assignees"], { cwd: this.cwd, encoding: "utf-8" });
2196
+ const { stdout: output } = await execFileAsync2("gh", [
2197
+ "issue",
2198
+ "view",
2199
+ String(issueNumber),
2200
+ "--repo",
2201
+ repo,
2202
+ "--json",
2203
+ "number,title,body,url,id,labels,assignees"
2204
+ ], { cwd: this.cwd, encoding: "utf-8" });
2074
2205
  rawIssue = JSON.parse(output);
2075
2206
  } catch {
2076
2207
  return null;
@@ -2175,6 +2306,9 @@ var init_local_kanban = __esm({
2175
2306
  });
2176
2307
  return toIBoardIssue(row);
2177
2308
  }
2309
+ async addIssue(_issueNumber, _column) {
2310
+ throw new Error("addIssue is not supported by the local Kanban provider.");
2311
+ }
2178
2312
  async getIssue(issueNumber) {
2179
2313
  const row = this.repo.getByNumber(issueNumber);
2180
2314
  return row ? toIBoardIssue(row) : null;
@@ -3046,6 +3180,9 @@ function auditLockPath(projectDir) {
3046
3180
  function plannerLockPath(projectDir) {
3047
3181
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
3048
3182
  }
3183
+ function analyticsLockPath(projectDir) {
3184
+ return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
3185
+ }
3049
3186
  function isProcessRunning(pid) {
3050
3187
  try {
3051
3188
  process.kill(pid, 0);
@@ -3404,7 +3541,8 @@ function collectLogInfo(projectDir) {
3404
3541
  { name: "reviewer", fileName: "reviewer.log" },
3405
3542
  { name: "qa", fileName: `${QA_LOG_NAME}.log` },
3406
3543
  { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
3407
- { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
3544
+ { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
3545
+ { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
3408
3546
  ];
3409
3547
  return logEntries.map(({ name, fileName }) => {
3410
3548
  const logPath = path6.join(projectDir, LOG_DIR, fileName);
@@ -3433,12 +3571,14 @@ async function fetchStatusSnapshot(projectDir, config) {
3433
3571
  const qaLock = checkLockFile(qaLockPath(projectDir));
3434
3572
  const auditLock = checkLockFile(auditLockPath(projectDir));
3435
3573
  const plannerLock = checkLockFile(plannerLockPath(projectDir));
3574
+ const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
3436
3575
  const processes = [
3437
3576
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
3438
3577
  { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
3439
3578
  { name: "qa", running: qaLock.running, pid: qaLock.pid },
3440
3579
  { name: "audit", running: auditLock.running, pid: auditLock.pid },
3441
- { name: "planner", running: plannerLock.running, pid: plannerLock.pid }
3580
+ { name: "planner", running: plannerLock.running, pid: plannerLock.pid },
3581
+ { name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid }
3442
3582
  ];
3443
3583
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
3444
3584
  const prs = await collectPrInfo(projectDir, config.branchPatterns);
@@ -4206,6 +4346,36 @@ var init_log_utils = __esm({
4206
4346
  }
4207
4347
  });
4208
4348
 
4349
+ // ../core/dist/utils/global-config.js
4350
+ import * as fs11 from "fs";
4351
+ import * as os5 from "os";
4352
+ import * as path10 from "path";
4353
+ function getGlobalNotificationsPath() {
4354
+ return path10.join(os5.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_NOTIFICATIONS_FILE_NAME);
4355
+ }
4356
+ function loadGlobalNotificationsConfig() {
4357
+ const filePath = getGlobalNotificationsPath();
4358
+ try {
4359
+ if (!fs11.existsSync(filePath))
4360
+ return { webhook: null };
4361
+ const raw = fs11.readFileSync(filePath, "utf-8");
4362
+ return JSON.parse(raw);
4363
+ } catch {
4364
+ return { webhook: null };
4365
+ }
4366
+ }
4367
+ function saveGlobalNotificationsConfig(config) {
4368
+ const filePath = getGlobalNotificationsPath();
4369
+ fs11.mkdirSync(path10.dirname(filePath), { recursive: true });
4370
+ fs11.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
4371
+ }
4372
+ var init_global_config = __esm({
4373
+ "../core/dist/utils/global-config.js"() {
4374
+ "use strict";
4375
+ init_constants();
4376
+ }
4377
+ });
4378
+
4209
4379
  // ../core/dist/utils/ui.js
4210
4380
  import chalk from "chalk";
4211
4381
  import ora from "ora";
@@ -4562,24 +4732,33 @@ async function sendWebhook(webhook, ctx) {
4562
4732
  warn(`Notification failed (${webhook.type}): ${message}`);
4563
4733
  }
4564
4734
  }
4735
+ function webhookIdentity(wh) {
4736
+ if (wh.type === "telegram")
4737
+ return `telegram:${wh.botToken}:${wh.chatId}`;
4738
+ return `${wh.type}:${wh.url}`;
4739
+ }
4565
4740
  async function sendNotifications(config, ctx) {
4566
- const webhooks = config.notifications?.webhooks ?? [];
4567
- const tasks = [];
4568
- for (const wh of webhooks) {
4569
- tasks.push(sendWebhook(wh, ctx));
4741
+ const projectWebhooks = config.notifications?.webhooks ?? [];
4742
+ const globalConfig = loadGlobalNotificationsConfig();
4743
+ const allWebhooks = [...projectWebhooks];
4744
+ if (globalConfig.webhook) {
4745
+ const projectIds = new Set(projectWebhooks.map(webhookIdentity));
4746
+ if (!projectIds.has(webhookIdentity(globalConfig.webhook))) {
4747
+ allWebhooks.push(globalConfig.webhook);
4748
+ }
4570
4749
  }
4571
- if (tasks.length === 0) {
4750
+ if (allWebhooks.length === 0) {
4572
4751
  return;
4573
4752
  }
4574
- const results = await Promise.allSettled(tasks);
4753
+ const results = await Promise.allSettled(allWebhooks.map((wh) => sendWebhook(wh, ctx)));
4575
4754
  const sent = results.filter((r) => r.status === "fulfilled").length;
4576
- const total = results.length;
4577
- info(`Sent ${sent}/${total} notifications`);
4755
+ info(`Sent ${sent}/${allWebhooks.length} notifications`);
4578
4756
  }
4579
4757
  var MAX_QA_SCREENSHOTS_IN_NOTIFICATION;
4580
4758
  var init_notify = __esm({
4581
4759
  "../core/dist/utils/notify.js"() {
4582
4760
  "use strict";
4761
+ init_global_config();
4583
4762
  init_ui();
4584
4763
  init_github();
4585
4764
  MAX_QA_SCREENSHOTS_IN_NOTIFICATION = 3;
@@ -4616,15 +4795,15 @@ var init_prd_discovery = __esm({
4616
4795
  });
4617
4796
 
4618
4797
  // ../core/dist/utils/prd-utils.js
4619
- import * as fs11 from "fs";
4620
- import * as path10 from "path";
4798
+ import * as fs12 from "fs";
4799
+ import * as path11 from "path";
4621
4800
  function slugify(name) {
4622
4801
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4623
4802
  }
4624
4803
  function getNextPrdNumber(prdDir) {
4625
- if (!fs11.existsSync(prdDir))
4804
+ if (!fs12.existsSync(prdDir))
4626
4805
  return 1;
4627
- const files = fs11.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
4806
+ const files = fs12.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
4628
4807
  const numbers = files.map((f) => {
4629
4808
  const match = f.match(/^(\d+)-/);
4630
4809
  return match ? parseInt(match[1], 10) : 0;
@@ -4632,16 +4811,16 @@ function getNextPrdNumber(prdDir) {
4632
4811
  return Math.max(0, ...numbers) + 1;
4633
4812
  }
4634
4813
  function markPrdDone(prdDir, prdFile) {
4635
- const sourcePath = path10.join(prdDir, prdFile);
4636
- if (!fs11.existsSync(sourcePath)) {
4814
+ const sourcePath = path11.join(prdDir, prdFile);
4815
+ if (!fs12.existsSync(sourcePath)) {
4637
4816
  return false;
4638
4817
  }
4639
- const doneDir = path10.join(prdDir, "done");
4640
- if (!fs11.existsSync(doneDir)) {
4641
- fs11.mkdirSync(doneDir, { recursive: true });
4818
+ const doneDir = path11.join(prdDir, "done");
4819
+ if (!fs12.existsSync(doneDir)) {
4820
+ fs12.mkdirSync(doneDir, { recursive: true });
4642
4821
  }
4643
- const destPath = path10.join(doneDir, prdFile);
4644
- fs11.renameSync(sourcePath, destPath);
4822
+ const destPath = path11.join(doneDir, prdFile);
4823
+ fs12.renameSync(sourcePath, destPath);
4645
4824
  return true;
4646
4825
  }
4647
4826
  var init_prd_utils = __esm({
@@ -4651,16 +4830,16 @@ var init_prd_utils = __esm({
4651
4830
  });
4652
4831
 
4653
4832
  // ../core/dist/utils/registry.js
4654
- import * as fs12 from "fs";
4655
- import * as os5 from "os";
4656
- import * as path11 from "path";
4833
+ import * as fs13 from "fs";
4834
+ import * as os6 from "os";
4835
+ import * as path12 from "path";
4657
4836
  function readLegacyRegistryEntries() {
4658
4837
  const registryPath = getRegistryPath();
4659
- if (!fs12.existsSync(registryPath)) {
4838
+ if (!fs13.existsSync(registryPath)) {
4660
4839
  return [];
4661
4840
  }
4662
4841
  try {
4663
- const raw = fs12.readFileSync(registryPath, "utf-8");
4842
+ const raw = fs13.readFileSync(registryPath, "utf-8");
4664
4843
  const parsed = JSON.parse(raw);
4665
4844
  if (!Array.isArray(parsed)) {
4666
4845
  return [];
@@ -4695,8 +4874,8 @@ function loadRegistryEntriesWithLegacyFallback() {
4695
4874
  return projectRegistry.getAll();
4696
4875
  }
4697
4876
  function getRegistryPath() {
4698
- const base = process.env.NIGHT_WATCH_HOME || path11.join(os5.homedir(), GLOBAL_CONFIG_DIR);
4699
- return path11.join(base, REGISTRY_FILE_NAME);
4877
+ const base = process.env.NIGHT_WATCH_HOME || path12.join(os6.homedir(), GLOBAL_CONFIG_DIR);
4878
+ return path12.join(base, REGISTRY_FILE_NAME);
4700
4879
  }
4701
4880
  function loadRegistry() {
4702
4881
  return loadRegistryEntriesWithLegacyFallback();
@@ -4709,7 +4888,7 @@ function saveRegistry(entries) {
4709
4888
  }
4710
4889
  }
4711
4890
  function registerProject(projectDir) {
4712
- const resolvedPath = path11.resolve(projectDir);
4891
+ const resolvedPath = path12.resolve(projectDir);
4713
4892
  const { projectRegistry } = getRepositories();
4714
4893
  const entries = loadRegistryEntriesWithLegacyFallback();
4715
4894
  const existing = entries.find((e) => e.path === resolvedPath);
@@ -4718,13 +4897,13 @@ function registerProject(projectDir) {
4718
4897
  }
4719
4898
  const name = getProjectName(resolvedPath);
4720
4899
  const nameExists = entries.some((e) => e.name === name);
4721
- const finalName = nameExists ? `${name}-${path11.basename(resolvedPath)}` : name;
4900
+ const finalName = nameExists ? `${name}-${path12.basename(resolvedPath)}` : name;
4722
4901
  const entry = { name: finalName, path: resolvedPath };
4723
4902
  projectRegistry.upsert(entry);
4724
4903
  return entry;
4725
4904
  }
4726
4905
  function unregisterProject(projectDir) {
4727
- const resolvedPath = path11.resolve(projectDir);
4906
+ const resolvedPath = path12.resolve(projectDir);
4728
4907
  loadRegistryEntriesWithLegacyFallback();
4729
4908
  const { projectRegistry } = getRepositories();
4730
4909
  return projectRegistry.remove(resolvedPath);
@@ -4734,7 +4913,7 @@ function validateRegistry() {
4734
4913
  const valid = [];
4735
4914
  const invalid = [];
4736
4915
  for (const entry of entries) {
4737
- if (fs12.existsSync(entry.path) && fs12.existsSync(path11.join(entry.path, CONFIG_FILE_NAME))) {
4916
+ if (fs13.existsSync(entry.path) && fs13.existsSync(path12.join(entry.path, CONFIG_FILE_NAME))) {
4738
4917
  valid.push(entry);
4739
4918
  } else {
4740
4919
  invalid.push(entry);
@@ -4922,18 +5101,18 @@ var init_roadmap_parser = __esm({
4922
5101
  });
4923
5102
 
4924
5103
  // ../core/dist/utils/roadmap-state.js
4925
- import * as fs13 from "fs";
4926
- import * as path12 from "path";
5104
+ import * as fs14 from "fs";
5105
+ import * as path13 from "path";
4927
5106
  function getStateFilePath(prdDir) {
4928
- return path12.join(prdDir, STATE_FILE_NAME);
5107
+ return path13.join(prdDir, STATE_FILE_NAME);
4929
5108
  }
4930
5109
  function readJsonState(prdDir) {
4931
5110
  const statePath = getStateFilePath(prdDir);
4932
- if (!fs13.existsSync(statePath)) {
5111
+ if (!fs14.existsSync(statePath)) {
4933
5112
  return null;
4934
5113
  }
4935
5114
  try {
4936
- const content = fs13.readFileSync(statePath, "utf-8");
5115
+ const content = fs14.readFileSync(statePath, "utf-8");
4937
5116
  const parsed = JSON.parse(content);
4938
5117
  if (typeof parsed !== "object" || parsed === null) {
4939
5118
  return null;
@@ -4971,11 +5150,11 @@ function saveRoadmapState(prdDir, state) {
4971
5150
  const { roadmapState } = getRepositories();
4972
5151
  roadmapState.save(prdDir, state);
4973
5152
  const statePath = getStateFilePath(prdDir);
4974
- const dir = path12.dirname(statePath);
4975
- if (!fs13.existsSync(dir)) {
4976
- fs13.mkdirSync(dir, { recursive: true });
5153
+ const dir = path13.dirname(statePath);
5154
+ if (!fs14.existsSync(dir)) {
5155
+ fs14.mkdirSync(dir, { recursive: true });
4977
5156
  }
4978
- fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5157
+ fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
4979
5158
  }
4980
5159
  function createEmptyState() {
4981
5160
  return {
@@ -5015,15 +5194,15 @@ var init_roadmap_state = __esm({
5015
5194
  });
5016
5195
 
5017
5196
  // ../core/dist/templates/slicer-prompt.js
5018
- import * as fs14 from "fs";
5019
- import * as path13 from "path";
5197
+ import * as fs15 from "fs";
5198
+ import * as path14 from "path";
5020
5199
  function loadSlicerTemplate(templateDir) {
5021
5200
  if (cachedTemplate) {
5022
5201
  return cachedTemplate;
5023
5202
  }
5024
- const templatePath = templateDir ? path13.join(templateDir, "slicer.md") : path13.resolve(__dirname, "..", "..", "templates", "slicer.md");
5203
+ const templatePath = templateDir ? path14.join(templateDir, "slicer.md") : path14.resolve(__dirname, "..", "..", "templates", "slicer.md");
5025
5204
  try {
5026
- cachedTemplate = fs14.readFileSync(templatePath, "utf-8");
5205
+ cachedTemplate = fs15.readFileSync(templatePath, "utf-8");
5027
5206
  return cachedTemplate;
5028
5207
  } catch (error2) {
5029
5208
  console.warn(`Warning: Could not load slicer template from ${templatePath}, using default:`, error2 instanceof Error ? error2.message : String(error2));
@@ -5048,7 +5227,7 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
5048
5227
  title,
5049
5228
  section,
5050
5229
  description: description || "(No description provided)",
5051
- outputFilePath: path13.join(prdDir, prdFilename),
5230
+ outputFilePath: path14.join(prdDir, prdFilename),
5052
5231
  prdDir
5053
5232
  };
5054
5233
  }
@@ -5142,8 +5321,8 @@ DO NOT forget to write the file.
5142
5321
  });
5143
5322
 
5144
5323
  // ../core/dist/utils/roadmap-scanner.js
5145
- import * as fs15 from "fs";
5146
- import * as path14 from "path";
5324
+ import * as fs16 from "fs";
5325
+ import * as path15 from "path";
5147
5326
  import { spawn } from "child_process";
5148
5327
  import { createHash as createHash3 } from "crypto";
5149
5328
  function normalizeAuditSeverity(raw) {
@@ -5244,11 +5423,11 @@ function auditFindingToRoadmapItem(finding) {
5244
5423
  };
5245
5424
  }
5246
5425
  function collectAuditPlannerItems(projectDir) {
5247
- const reportPath = path14.join(projectDir, "logs", "audit-report.md");
5248
- if (!fs15.existsSync(reportPath)) {
5426
+ const reportPath = path15.join(projectDir, "logs", "audit-report.md");
5427
+ if (!fs16.existsSync(reportPath)) {
5249
5428
  return [];
5250
5429
  }
5251
- const reportContent = fs15.readFileSync(reportPath, "utf-8");
5430
+ const reportContent = fs16.readFileSync(reportPath, "utf-8");
5252
5431
  if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
5253
5432
  return [];
5254
5433
  }
@@ -5257,9 +5436,9 @@ function collectAuditPlannerItems(projectDir) {
5257
5436
  return findings.map(auditFindingToRoadmapItem);
5258
5437
  }
5259
5438
  function getRoadmapStatus(projectDir, config) {
5260
- const roadmapPath = path14.join(projectDir, config.roadmapScanner.roadmapPath);
5439
+ const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
5261
5440
  const scannerEnabled = config.roadmapScanner.enabled;
5262
- if (!fs15.existsSync(roadmapPath)) {
5441
+ if (!fs16.existsSync(roadmapPath)) {
5263
5442
  return {
5264
5443
  found: false,
5265
5444
  enabled: scannerEnabled,
@@ -5270,9 +5449,9 @@ function getRoadmapStatus(projectDir, config) {
5270
5449
  items: []
5271
5450
  };
5272
5451
  }
5273
- const content = fs15.readFileSync(roadmapPath, "utf-8");
5452
+ const content = fs16.readFileSync(roadmapPath, "utf-8");
5274
5453
  const items = parseRoadmap(content);
5275
- const prdDir = path14.join(projectDir, config.prdDir);
5454
+ const prdDir = path15.join(projectDir, config.prdDir);
5276
5455
  const state = loadRoadmapState(prdDir);
5277
5456
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5278
5457
  const statusItems = items.map((item) => {
@@ -5309,10 +5488,10 @@ function getRoadmapStatus(projectDir, config) {
5309
5488
  }
5310
5489
  function scanExistingPrdSlugs(prdDir) {
5311
5490
  const slugs = /* @__PURE__ */ new Set();
5312
- if (!fs15.existsSync(prdDir)) {
5491
+ if (!fs16.existsSync(prdDir)) {
5313
5492
  return slugs;
5314
5493
  }
5315
- const files = fs15.readdirSync(prdDir);
5494
+ const files = fs16.readdirSync(prdDir);
5316
5495
  for (const file of files) {
5317
5496
  if (!file.endsWith(".md")) {
5318
5497
  continue;
@@ -5348,20 +5527,20 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5348
5527
  const nextNum = getNextPrdNumber(prdDir);
5349
5528
  const padded = String(nextNum).padStart(2, "0");
5350
5529
  const filename = `${padded}-${itemSlug}.md`;
5351
- const filePath = path14.join(prdDir, filename);
5352
- if (!fs15.existsSync(prdDir)) {
5353
- fs15.mkdirSync(prdDir, { recursive: true });
5530
+ const filePath = path15.join(prdDir, filename);
5531
+ if (!fs16.existsSync(prdDir)) {
5532
+ fs16.mkdirSync(prdDir, { recursive: true });
5354
5533
  }
5355
5534
  const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
5356
5535
  const prompt2 = renderSlicerPrompt(promptVars);
5357
5536
  const provider = resolveJobProvider(config, "slicer");
5358
5537
  const providerArgs = buildProviderArgs(provider, prompt2, projectDir);
5359
- const logDir = path14.join(projectDir, "logs");
5360
- if (!fs15.existsSync(logDir)) {
5361
- fs15.mkdirSync(logDir, { recursive: true });
5538
+ const logDir = path15.join(projectDir, "logs");
5539
+ if (!fs16.existsSync(logDir)) {
5540
+ fs16.mkdirSync(logDir, { recursive: true });
5362
5541
  }
5363
- const logFile = path14.join(logDir, `slicer-${itemSlug}.log`);
5364
- const logStream = fs15.createWriteStream(logFile, { flags: "w" });
5542
+ const logFile = path15.join(logDir, `slicer-${itemSlug}.log`);
5543
+ const logStream = fs16.createWriteStream(logFile, { flags: "w" });
5365
5544
  logStream.on("error", () => {
5366
5545
  });
5367
5546
  return new Promise((resolve10) => {
@@ -5398,7 +5577,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5398
5577
  });
5399
5578
  return;
5400
5579
  }
5401
- if (!fs15.existsSync(filePath)) {
5580
+ if (!fs16.existsSync(filePath)) {
5402
5581
  resolve10({
5403
5582
  sliced: false,
5404
5583
  error: `Provider did not create expected file: ${filePath}`,
@@ -5421,23 +5600,23 @@ async function sliceNextItem(projectDir, config) {
5421
5600
  error: "Roadmap scanner is disabled"
5422
5601
  };
5423
5602
  }
5424
- const roadmapPath = path14.join(projectDir, config.roadmapScanner.roadmapPath);
5603
+ const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
5425
5604
  const auditItems = collectAuditPlannerItems(projectDir);
5426
- const roadmapExists = fs15.existsSync(roadmapPath);
5605
+ const roadmapExists = fs16.existsSync(roadmapPath);
5427
5606
  if (!roadmapExists && auditItems.length === 0) {
5428
5607
  return {
5429
5608
  sliced: false,
5430
5609
  error: "No pending items to process"
5431
5610
  };
5432
5611
  }
5433
- const roadmapItems = roadmapExists ? parseRoadmap(fs15.readFileSync(roadmapPath, "utf-8")) : [];
5612
+ const roadmapItems = roadmapExists ? parseRoadmap(fs16.readFileSync(roadmapPath, "utf-8")) : [];
5434
5613
  if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
5435
5614
  return {
5436
5615
  sliced: false,
5437
5616
  error: "No items in roadmap"
5438
5617
  };
5439
5618
  }
5440
- const prdDir = path14.join(projectDir, config.prdDir);
5619
+ const prdDir = path15.join(projectDir, config.prdDir);
5441
5620
  const state = loadRoadmapState(prdDir);
5442
5621
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5443
5622
  const pickEligibleItem = (items) => {
@@ -5608,8 +5787,8 @@ var init_shell = __esm({
5608
5787
  });
5609
5788
 
5610
5789
  // ../core/dist/utils/scheduling.js
5611
- import * as fs16 from "fs";
5612
- import * as path15 from "path";
5790
+ import * as fs17 from "fs";
5791
+ import * as path16 from "path";
5613
5792
  function normalizeSchedulingPriority(priority) {
5614
5793
  if (!Number.isFinite(priority)) {
5615
5794
  return DEFAULT_SCHEDULING_PRIORITY;
@@ -5628,12 +5807,14 @@ function isJobTypeEnabled(config, jobType) {
5628
5807
  return config.audit.enabled;
5629
5808
  case "slicer":
5630
5809
  return config.roadmapScanner.enabled;
5810
+ case "analytics":
5811
+ return config.analytics.enabled;
5631
5812
  default:
5632
5813
  return true;
5633
5814
  }
5634
5815
  }
5635
5816
  function loadPeerConfig(projectPath) {
5636
- if (!fs16.existsSync(projectPath) || !fs16.existsSync(path15.join(projectPath, CONFIG_FILE_NAME))) {
5817
+ if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
5637
5818
  return null;
5638
5819
  }
5639
5820
  try {
@@ -5644,9 +5825,9 @@ function loadPeerConfig(projectPath) {
5644
5825
  }
5645
5826
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5646
5827
  const peers = /* @__PURE__ */ new Map();
5647
- const currentPath = path15.resolve(currentProjectDir);
5828
+ const currentPath = path16.resolve(currentProjectDir);
5648
5829
  const addPeer = (projectPath, config) => {
5649
- const resolvedPath = path15.resolve(projectPath);
5830
+ const resolvedPath = path16.resolve(projectPath);
5650
5831
  if (!isJobTypeEnabled(config, jobType)) {
5651
5832
  return;
5652
5833
  }
@@ -5654,12 +5835,12 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5654
5835
  path: resolvedPath,
5655
5836
  config,
5656
5837
  schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
5657
- sortKey: `${path15.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
5838
+ sortKey: `${path16.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
5658
5839
  });
5659
5840
  };
5660
5841
  addPeer(currentPath, currentConfig);
5661
5842
  for (const entry of loadRegistry()) {
5662
- const resolvedPath = path15.resolve(entry.path);
5843
+ const resolvedPath = path16.resolve(entry.path);
5663
5844
  if (resolvedPath === currentPath || peers.has(resolvedPath)) {
5664
5845
  continue;
5665
5846
  }
@@ -5677,7 +5858,7 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5677
5858
  }
5678
5859
  function getSchedulingPlan(projectDir, config, jobType) {
5679
5860
  const peers = collectSchedulingPeers(projectDir, config, jobType);
5680
- const currentPath = path15.resolve(projectDir);
5861
+ const currentPath = path16.resolve(projectDir);
5681
5862
  const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
5682
5863
  const peerCount = Math.max(1, peers.length);
5683
5864
  const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
@@ -5769,8 +5950,8 @@ var init_webhook_validator = __esm({
5769
5950
 
5770
5951
  // ../core/dist/utils/worktree-manager.js
5771
5952
  import { execFileSync as execFileSync4 } from "child_process";
5772
- import * as fs17 from "fs";
5773
- import * as path16 from "path";
5953
+ import * as fs18 from "fs";
5954
+ import * as path17 from "path";
5774
5955
  function gitExec(args, cwd, logFile) {
5775
5956
  try {
5776
5957
  const result = execFileSync4("git", args, {
@@ -5780,7 +5961,7 @@ function gitExec(args, cwd, logFile) {
5780
5961
  });
5781
5962
  if (logFile && result) {
5782
5963
  try {
5783
- fs17.appendFileSync(logFile, result);
5964
+ fs18.appendFileSync(logFile, result);
5784
5965
  } catch {
5785
5966
  }
5786
5967
  }
@@ -5789,7 +5970,7 @@ function gitExec(args, cwd, logFile) {
5789
5970
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
5790
5971
  if (logFile) {
5791
5972
  try {
5792
- fs17.appendFileSync(logFile, errorMessage + "\n");
5973
+ fs18.appendFileSync(logFile, errorMessage + "\n");
5793
5974
  } catch {
5794
5975
  }
5795
5976
  }
@@ -5814,11 +5995,11 @@ function branchExistsRemotely(projectDir, branchName) {
5814
5995
  }
5815
5996
  function prepareBranchWorktree(options) {
5816
5997
  const { projectDir, worktreeDir, branchName, defaultBranch, logFile } = options;
5817
- if (fs17.existsSync(worktreeDir)) {
5998
+ if (fs18.existsSync(worktreeDir)) {
5818
5999
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
5819
6000
  if (!isRegistered) {
5820
6001
  try {
5821
- fs17.rmSync(worktreeDir, { recursive: true, force: true });
6002
+ fs18.rmSync(worktreeDir, { recursive: true, force: true });
5822
6003
  } catch {
5823
6004
  }
5824
6005
  }
@@ -5857,11 +6038,11 @@ function prepareBranchWorktree(options) {
5857
6038
  }
5858
6039
  function prepareDetachedWorktree(options) {
5859
6040
  const { projectDir, worktreeDir, defaultBranch, logFile } = options;
5860
- if (fs17.existsSync(worktreeDir)) {
6041
+ if (fs18.existsSync(worktreeDir)) {
5861
6042
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
5862
6043
  if (!isRegistered) {
5863
6044
  try {
5864
- fs17.rmSync(worktreeDir, { recursive: true, force: true });
6045
+ fs18.rmSync(worktreeDir, { recursive: true, force: true });
5865
6046
  } catch {
5866
6047
  }
5867
6048
  }
@@ -5903,7 +6084,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
5903
6084
  }
5904
6085
  }
5905
6086
  function cleanupWorktrees(projectDir, scope) {
5906
- const projectName = path16.basename(projectDir);
6087
+ const projectName = path17.basename(projectDir);
5907
6088
  const matchToken = scope ? scope : `${projectName}-nw`;
5908
6089
  const removed = [];
5909
6090
  try {
@@ -5938,16 +6119,16 @@ var init_worktree_manager = __esm({
5938
6119
  });
5939
6120
 
5940
6121
  // ../core/dist/utils/job-queue.js
5941
- import * as os6 from "os";
5942
- import * as path17 from "path";
6122
+ import * as os7 from "os";
6123
+ import * as path18 from "path";
5943
6124
  import Database7 from "better-sqlite3";
5944
6125
  function getStateDbPath() {
5945
- const base = process.env.NIGHT_WATCH_HOME || path17.join(os6.homedir(), GLOBAL_CONFIG_DIR);
5946
- return path17.join(base, STATE_DB_FILE_NAME);
6126
+ const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6127
+ return path18.join(base, STATE_DB_FILE_NAME);
5947
6128
  }
5948
6129
  function getQueueLockPath() {
5949
- const base = process.env.NIGHT_WATCH_HOME || path17.join(os6.homedir(), GLOBAL_CONFIG_DIR);
5950
- return path17.join(base, QUEUE_LOCK_FILE_NAME);
6130
+ const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6131
+ return path18.join(base, QUEUE_LOCK_FILE_NAME);
5951
6132
  }
5952
6133
  function openDb() {
5953
6134
  const dbPath = getStateDbPath();
@@ -5987,6 +6168,8 @@ function getLockPathForJob(projectPath, jobType) {
5987
6168
  return auditLockPath(projectPath);
5988
6169
  case "slicer":
5989
6170
  return plannerLockPath(projectPath);
6171
+ case "analytics":
6172
+ return analyticsLockPath(projectPath);
5990
6173
  }
5991
6174
  }
5992
6175
  function reconcileStaleRunningJobs(db) {
@@ -6166,7 +6349,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
6166
6349
  }
6167
6350
  const bucketConfig = config?.providerBuckets?.[bucketKey];
6168
6351
  if (!bucketConfig) {
6169
- logger.debug("Capacity check skipped: bucket not configured", { id: candidate.id, bucket: bucketKey });
6352
+ logger.debug("Capacity check skipped: bucket not configured", {
6353
+ id: candidate.id,
6354
+ bucket: bucketKey
6355
+ });
6170
6356
  return true;
6171
6357
  }
6172
6358
  const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
@@ -6197,7 +6383,10 @@ function dispatchNextJob(config) {
6197
6383
  const runningCount = running?.count ?? 0;
6198
6384
  logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
6199
6385
  if (runningCount >= maxConcurrency) {
6200
- logger.info("Dispatch skipped: global concurrency limit reached", { runningCount, maxConcurrency });
6386
+ logger.info("Dispatch skipped: global concurrency limit reached", {
6387
+ runningCount,
6388
+ maxConcurrency
6389
+ });
6201
6390
  return null;
6202
6391
  }
6203
6392
  const now = Math.floor(Date.now() / 1e3);
@@ -6223,7 +6412,9 @@ function dispatchNextJob(config) {
6223
6412
  logger.debug("Dispatch skipped: no pending jobs");
6224
6413
  return null;
6225
6414
  }
6226
- logger.debug("Provider-aware dispatch: evaluating candidates", { candidateCount: candidates.length });
6415
+ logger.debug("Provider-aware dispatch: evaluating candidates", {
6416
+ candidateCount: candidates.length
6417
+ });
6227
6418
  const inFlightByBucket = getInFlightCountByBucket(db);
6228
6419
  for (const candidate of candidates) {
6229
6420
  if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
@@ -6299,7 +6490,11 @@ function clearQueue(filter, force) {
6299
6490
  } else {
6300
6491
  result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
6301
6492
  }
6302
- logger.info("Queue cleared", { count: result.changes, filter: filter ?? "all", force: force ?? false });
6493
+ logger.info("Queue cleared", {
6494
+ count: result.changes,
6495
+ filter: filter ?? "all",
6496
+ force: force ?? false
6497
+ });
6303
6498
  return result.changes;
6304
6499
  } finally {
6305
6500
  db.close();
@@ -6432,6 +6627,192 @@ var init_job_queue = __esm({
6432
6627
  }
6433
6628
  });
6434
6629
 
6630
+ // ../core/dist/analytics/amplitude-client.js
6631
+ function buildAuthHeader(apiKey, secretKey) {
6632
+ return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
6633
+ }
6634
+ function buildDateRange(lookbackDays) {
6635
+ const end = /* @__PURE__ */ new Date();
6636
+ const start = /* @__PURE__ */ new Date();
6637
+ start.setDate(start.getDate() - lookbackDays);
6638
+ const fmt = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
6639
+ return { start: fmt(start), end: fmt(end) };
6640
+ }
6641
+ async function amplitudeFetch(url, authHeader, label2) {
6642
+ logger2.debug(`Fetching ${label2}`, { url });
6643
+ const response = await fetch(url, {
6644
+ headers: { Authorization: authHeader }
6645
+ });
6646
+ if (!response.ok) {
6647
+ if (response.status === 401) {
6648
+ throw new Error(`Amplitude authentication failed (401). Check your API Key and Secret Key.`);
6649
+ }
6650
+ if (response.status === 429) {
6651
+ throw new Error(`Amplitude rate limit exceeded (429). Try again later.`);
6652
+ }
6653
+ throw new Error(`Amplitude API error: ${response.status} ${response.statusText}`);
6654
+ }
6655
+ return response.json();
6656
+ }
6657
+ async function fetchAmplitudeData(apiKey, secretKey, lookbackDays) {
6658
+ const authHeader = buildAuthHeader(apiKey, secretKey);
6659
+ const { start, end } = buildDateRange(lookbackDays);
6660
+ logger2.info("Fetching Amplitude data", { lookbackDays, start, end });
6661
+ const baseUrl = "https://amplitude.com/api/2";
6662
+ const allEventsParam = encodeURIComponent('{"event_type":"_all"}');
6663
+ const [activeUsers, eventSegmentation, retention, userSessions] = await Promise.allSettled([
6664
+ amplitudeFetch(`${baseUrl}/users/active?start=${start}&end=${end}`, authHeader, "active users"),
6665
+ amplitudeFetch(`${baseUrl}/events/segmentation?start=${start}&end=${end}&e=${allEventsParam}`, authHeader, "event segmentation"),
6666
+ amplitudeFetch(`${baseUrl}/retention?se=${allEventsParam}&re=${allEventsParam}&start=${start}&end=${end}`, authHeader, "retention"),
6667
+ amplitudeFetch(`${baseUrl}/sessions/average?start=${start}&end=${end}`, authHeader, "user sessions")
6668
+ ]);
6669
+ const settled = [activeUsers, eventSegmentation, retention, userSessions];
6670
+ const labels = ["active users", "event segmentation", "retention", "user sessions"];
6671
+ if (settled.every((r) => r.status === "rejected")) {
6672
+ throw settled[0].reason;
6673
+ }
6674
+ const extract = (result, label2) => {
6675
+ if (result.status === "fulfilled")
6676
+ return result.value;
6677
+ logger2.warn(`Failed to fetch ${label2}`, { error: String(result.reason) });
6678
+ return null;
6679
+ };
6680
+ return {
6681
+ activeUsers: extract(activeUsers, labels[0]),
6682
+ eventSegmentation: extract(eventSegmentation, labels[1]),
6683
+ retention: extract(retention, labels[2]),
6684
+ userSessions: extract(userSessions, labels[3]),
6685
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
6686
+ lookbackDays
6687
+ };
6688
+ }
6689
+ var logger2;
6690
+ var init_amplitude_client = __esm({
6691
+ "../core/dist/analytics/amplitude-client.js"() {
6692
+ "use strict";
6693
+ init_logger();
6694
+ logger2 = createLogger("amplitude-client");
6695
+ }
6696
+ });
6697
+
6698
+ // ../core/dist/analytics/analytics-runner.js
6699
+ import * as fs19 from "fs";
6700
+ import * as os8 from "os";
6701
+ import * as path19 from "path";
6702
+ function parseIssuesFromResponse(text) {
6703
+ const start = text.indexOf("[");
6704
+ const end = text.lastIndexOf("]");
6705
+ if (start === -1 || end === -1 || end <= start)
6706
+ return [];
6707
+ try {
6708
+ const parsed = JSON.parse(text.slice(start, end + 1));
6709
+ if (!Array.isArray(parsed))
6710
+ return [];
6711
+ return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.title === "string" && typeof item.body === "string");
6712
+ } catch {
6713
+ logger3.warn("Failed to parse AI response as JSON");
6714
+ return [];
6715
+ }
6716
+ }
6717
+ async function runAnalytics(config, projectDir) {
6718
+ const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
6719
+ const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
6720
+ if (!apiKey || !secretKey) {
6721
+ throw new Error("AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics");
6722
+ }
6723
+ logger3.info("Fetching Amplitude data", { lookbackDays: config.analytics.lookbackDays });
6724
+ const data = await fetchAmplitudeData(apiKey, secretKey, config.analytics.lookbackDays);
6725
+ const systemPrompt = config.analytics.analysisPrompt?.trim() || DEFAULT_ANALYTICS_PROMPT;
6726
+ const prompt2 = `${systemPrompt}
6727
+
6728
+ --- AMPLITUDE DATA ---
6729
+ ${JSON.stringify(data, null, 2)}`;
6730
+ const tmpDir = fs19.mkdtempSync(path19.join(os8.tmpdir(), "nw-analytics-"));
6731
+ const promptFile = path19.join(tmpDir, "analytics-prompt.md");
6732
+ fs19.writeFileSync(promptFile, prompt2, "utf-8");
6733
+ try {
6734
+ const provider = resolveJobProvider(config, "analytics");
6735
+ const providerCmd = PROVIDER_COMMANDS[provider];
6736
+ let scriptContent;
6737
+ if (provider === "claude") {
6738
+ const modelId = CLAUDE_MODEL_IDS[config.claudeModel ?? "sonnet"];
6739
+ scriptContent = `#!/usr/bin/env bash
6740
+ set -euo pipefail
6741
+ ${providerCmd} -p "$(cat ${promptFile})" --model ${modelId} --dangerously-skip-permissions 2>&1
6742
+ `;
6743
+ } else {
6744
+ scriptContent = `#!/usr/bin/env bash
6745
+ set -euo pipefail
6746
+ ${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
6747
+ `;
6748
+ }
6749
+ const scriptFile = path19.join(tmpDir, "run-analytics.sh");
6750
+ fs19.writeFileSync(scriptFile, scriptContent, { mode: 493 });
6751
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
6752
+ if (exitCode !== 0) {
6753
+ throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
6754
+ }
6755
+ const fullOutput = `${stdout}
6756
+ ${stderr}`;
6757
+ const issues = parseIssuesFromResponse(fullOutput);
6758
+ if (issues.length === 0) {
6759
+ logger3.info("No actionable insights found");
6760
+ return { issuesCreated: 0, summary: "No actionable insights found" };
6761
+ }
6762
+ const boardProvider = createBoardProvider(config.boardProvider, projectDir);
6763
+ const targetColumn = config.analytics.targetColumn;
6764
+ let created = 0;
6765
+ for (const issue of issues) {
6766
+ try {
6767
+ await boardProvider.createIssue({
6768
+ title: issue.title,
6769
+ body: issue.body,
6770
+ column: targetColumn,
6771
+ labels: issue.labels ?? ["analytics"]
6772
+ });
6773
+ created++;
6774
+ logger3.info("Created board issue", { title: issue.title, column: targetColumn });
6775
+ } catch (err) {
6776
+ logger3.error("Failed to create board issue", {
6777
+ title: issue.title,
6778
+ error: String(err)
6779
+ });
6780
+ }
6781
+ }
6782
+ return {
6783
+ issuesCreated: created,
6784
+ summary: `Created ${created} issue(s) from analytics insights`
6785
+ };
6786
+ } finally {
6787
+ try {
6788
+ fs19.rmSync(tmpDir, { recursive: true, force: true });
6789
+ } catch {
6790
+ }
6791
+ }
6792
+ }
6793
+ var logger3;
6794
+ var init_analytics_runner = __esm({
6795
+ "../core/dist/analytics/analytics-runner.js"() {
6796
+ "use strict";
6797
+ init_factory();
6798
+ init_config();
6799
+ init_constants();
6800
+ init_shell();
6801
+ init_logger();
6802
+ init_amplitude_client();
6803
+ logger3 = createLogger("analytics");
6804
+ }
6805
+ });
6806
+
6807
+ // ../core/dist/analytics/index.js
6808
+ var init_analytics = __esm({
6809
+ "../core/dist/analytics/index.js"() {
6810
+ "use strict";
6811
+ init_amplitude_client();
6812
+ init_analytics_runner();
6813
+ }
6814
+ });
6815
+
6435
6816
  // ../core/dist/templates/prd-template.js
6436
6817
  function renderDependsOn(deps) {
6437
6818
  if (deps.length === 0) {
@@ -6608,6 +6989,7 @@ sequenceDiagram
6608
6989
  // ../core/dist/index.js
6609
6990
  var dist_exports = {};
6610
6991
  __export(dist_exports, {
6992
+ ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
6611
6993
  AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
6612
6994
  BOARD_COLUMNS: () => BOARD_COLUMNS,
6613
6995
  BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
@@ -6619,6 +7001,13 @@ __export(dist_exports, {
6619
7001
  CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
6620
7002
  CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
6621
7003
  DATABASE_TOKEN: () => DATABASE_TOKEN,
7004
+ DEFAULT_ANALYTICS: () => DEFAULT_ANALYTICS,
7005
+ DEFAULT_ANALYTICS_ENABLED: () => DEFAULT_ANALYTICS_ENABLED,
7006
+ DEFAULT_ANALYTICS_LOOKBACK_DAYS: () => DEFAULT_ANALYTICS_LOOKBACK_DAYS,
7007
+ DEFAULT_ANALYTICS_MAX_RUNTIME: () => DEFAULT_ANALYTICS_MAX_RUNTIME,
7008
+ DEFAULT_ANALYTICS_PROMPT: () => DEFAULT_ANALYTICS_PROMPT,
7009
+ DEFAULT_ANALYTICS_SCHEDULE: () => DEFAULT_ANALYTICS_SCHEDULE,
7010
+ DEFAULT_ANALYTICS_TARGET_COLUMN: () => DEFAULT_ANALYTICS_TARGET_COLUMN,
6622
7011
  DEFAULT_AUDIT: () => DEFAULT_AUDIT,
6623
7012
  DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
6624
7013
  DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
@@ -6674,6 +7063,7 @@ __export(dist_exports, {
6674
7063
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
6675
7064
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
6676
7065
  GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
7066
+ GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
6677
7067
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
6678
7068
  HORIZON_LABELS: () => HORIZON_LABELS,
6679
7069
  HORIZON_LABEL_INFO: () => HORIZON_LABEL_INFO,
@@ -6705,6 +7095,7 @@ __export(dist_exports, {
6705
7095
  acquireLock: () => acquireLock,
6706
7096
  addDelayToIsoString: () => addDelayToIsoString,
6707
7097
  addEntry: () => addEntry,
7098
+ analyticsLockPath: () => analyticsLockPath,
6708
7099
  auditLockPath: () => auditLockPath,
6709
7100
  buildDescription: () => buildDescription,
6710
7101
  calculateStringSimilarity: () => calculateStringSimilarity,
@@ -6755,6 +7146,7 @@ __export(dist_exports, {
6755
7146
  extractPriority: () => extractPriority,
6756
7147
  extractQaScreenshotUrls: () => extractQaScreenshotUrls,
6757
7148
  extractSummary: () => extractSummary,
7149
+ fetchAmplitudeData: () => fetchAmplitudeData,
6758
7150
  fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
6759
7151
  fetchPrDetails: () => fetchPrDetails,
6760
7152
  fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
@@ -6825,6 +7217,7 @@ __export(dist_exports, {
6825
7217
  label: () => label,
6826
7218
  listPrdStatesByStatus: () => listPrdStatesByStatus,
6827
7219
  loadConfig: () => loadConfig,
7220
+ loadGlobalNotificationsConfig: () => loadGlobalNotificationsConfig,
6828
7221
  loadHistory: () => loadHistory,
6829
7222
  loadRegistry: () => loadRegistry,
6830
7223
  loadRoadmapState: () => loadRoadmapState,
@@ -6862,8 +7255,10 @@ __export(dist_exports, {
6862
7255
  reviewerLockPath: () => reviewerLockPath,
6863
7256
  rotateLog: () => rotateLog,
6864
7257
  runAllChecks: () => runAllChecks,
7258
+ runAnalytics: () => runAnalytics,
6865
7259
  runMigrations: () => runMigrations,
6866
7260
  saveConfig: () => saveConfig,
7261
+ saveGlobalNotificationsConfig: () => saveGlobalNotificationsConfig,
6867
7262
  saveHistory: () => saveHistory,
6868
7263
  saveRegistry: () => saveRegistry,
6869
7264
  saveRoadmapState: () => saveRoadmapState,
@@ -6913,6 +7308,7 @@ var init_dist = __esm({
6913
7308
  init_git_utils();
6914
7309
  init_github();
6915
7310
  init_log_utils();
7311
+ init_global_config();
6916
7312
  init_notify();
6917
7313
  init_prd_discovery();
6918
7314
  init_prd_states();
@@ -6930,6 +7326,7 @@ var init_dist = __esm({
6930
7326
  init_webhook_validator();
6931
7327
  init_worktree_manager();
6932
7328
  init_job_queue();
7329
+ init_analytics();
6933
7330
  init_prd_template();
6934
7331
  init_slicer_prompt();
6935
7332
  }
@@ -6938,39 +7335,39 @@ var init_dist = __esm({
6938
7335
  // src/cli.ts
6939
7336
  import "reflect-metadata";
6940
7337
  import { Command as Command3 } from "commander";
6941
- import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
7338
+ import { existsSync as existsSync30, readFileSync as readFileSync19 } from "fs";
6942
7339
  import { fileURLToPath as fileURLToPath4 } from "url";
6943
- import { dirname as dirname8, join as join34 } from "path";
7340
+ import { dirname as dirname9, join as join36 } from "path";
6944
7341
 
6945
7342
  // src/commands/init.ts
6946
7343
  init_dist();
6947
- import fs18 from "fs";
6948
- import path18 from "path";
7344
+ import fs20 from "fs";
7345
+ import path20 from "path";
6949
7346
  import { execSync as execSync3 } from "child_process";
6950
7347
  import { fileURLToPath as fileURLToPath2 } from "url";
6951
- import { dirname as dirname4, join as join16 } from "path";
7348
+ import { dirname as dirname5, join as join18 } from "path";
6952
7349
  import * as readline from "readline";
6953
7350
  var __filename = fileURLToPath2(import.meta.url);
6954
- var __dirname2 = dirname4(__filename);
7351
+ var __dirname2 = dirname5(__filename);
6955
7352
  function findTemplatesDir(startDir) {
6956
7353
  let d = startDir;
6957
7354
  for (let i = 0; i < 8; i++) {
6958
- const candidate = join16(d, "templates");
6959
- if (fs18.existsSync(candidate) && fs18.statSync(candidate).isDirectory()) {
7355
+ const candidate = join18(d, "templates");
7356
+ if (fs20.existsSync(candidate) && fs20.statSync(candidate).isDirectory()) {
6960
7357
  return candidate;
6961
7358
  }
6962
- d = dirname4(d);
7359
+ d = dirname5(d);
6963
7360
  }
6964
- return join16(startDir, "templates");
7361
+ return join18(startDir, "templates");
6965
7362
  }
6966
7363
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
6967
7364
  function hasPlaywrightDependency(cwd) {
6968
- const packageJsonPath = path18.join(cwd, "package.json");
6969
- if (!fs18.existsSync(packageJsonPath)) {
7365
+ const packageJsonPath = path20.join(cwd, "package.json");
7366
+ if (!fs20.existsSync(packageJsonPath)) {
6970
7367
  return false;
6971
7368
  }
6972
7369
  try {
6973
- const packageJson2 = JSON.parse(fs18.readFileSync(packageJsonPath, "utf-8"));
7370
+ const packageJson2 = JSON.parse(fs20.readFileSync(packageJsonPath, "utf-8"));
6974
7371
  return Boolean(
6975
7372
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
6976
7373
  );
@@ -6982,7 +7379,7 @@ function detectPlaywright(cwd) {
6982
7379
  if (hasPlaywrightDependency(cwd)) {
6983
7380
  return true;
6984
7381
  }
6985
- if (fs18.existsSync(path18.join(cwd, "node_modules", ".bin", "playwright"))) {
7382
+ if (fs20.existsSync(path20.join(cwd, "node_modules", ".bin", "playwright"))) {
6986
7383
  return true;
6987
7384
  }
6988
7385
  try {
@@ -6998,10 +7395,10 @@ function detectPlaywright(cwd) {
6998
7395
  }
6999
7396
  }
7000
7397
  function resolvePlaywrightInstallCommand(cwd) {
7001
- if (fs18.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
7398
+ if (fs20.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) {
7002
7399
  return "pnpm add -D @playwright/test";
7003
7400
  }
7004
- if (fs18.existsSync(path18.join(cwd, "yarn.lock"))) {
7401
+ if (fs20.existsSync(path20.join(cwd, "yarn.lock"))) {
7005
7402
  return "yarn add -D @playwright/test";
7006
7403
  }
7007
7404
  return "npm install -D @playwright/test";
@@ -7147,8 +7544,8 @@ function promptProviderSelection(providers) {
7147
7544
  });
7148
7545
  }
7149
7546
  function ensureDir(dirPath) {
7150
- if (!fs18.existsSync(dirPath)) {
7151
- fs18.mkdirSync(dirPath, { recursive: true });
7547
+ if (!fs20.existsSync(dirPath)) {
7548
+ fs20.mkdirSync(dirPath, { recursive: true });
7152
7549
  }
7153
7550
  }
7154
7551
  function buildInitConfig(params) {
@@ -7195,6 +7592,7 @@ function buildInitConfig(params) {
7195
7592
  branchPatterns: [...defaults.qa.branchPatterns]
7196
7593
  },
7197
7594
  audit: { ...defaults.audit },
7595
+ analytics: { ...defaults.analytics },
7198
7596
  jobProviders: { ...defaults.jobProviders },
7199
7597
  queue: {
7200
7598
  ...defaults.queue,
@@ -7204,30 +7602,30 @@ function buildInitConfig(params) {
7204
7602
  }
7205
7603
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
7206
7604
  if (customTemplatesDir !== null) {
7207
- const customPath = join16(customTemplatesDir, templateName);
7208
- if (fs18.existsSync(customPath)) {
7605
+ const customPath = join18(customTemplatesDir, templateName);
7606
+ if (fs20.existsSync(customPath)) {
7209
7607
  return { path: customPath, source: "custom" };
7210
7608
  }
7211
7609
  }
7212
- return { path: join16(bundledTemplatesDir, templateName), source: "bundled" };
7610
+ return { path: join18(bundledTemplatesDir, templateName), source: "bundled" };
7213
7611
  }
7214
7612
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
7215
- if (fs18.existsSync(targetPath) && !force) {
7613
+ if (fs20.existsSync(targetPath) && !force) {
7216
7614
  console.log(` Skipped (exists): ${targetPath}`);
7217
7615
  return { created: false, source: source ?? "bundled" };
7218
7616
  }
7219
- const templatePath = sourcePath ?? join16(TEMPLATES_DIR, templateName);
7617
+ const templatePath = sourcePath ?? join18(TEMPLATES_DIR, templateName);
7220
7618
  const resolvedSource = source ?? "bundled";
7221
- let content = fs18.readFileSync(templatePath, "utf-8");
7619
+ let content = fs20.readFileSync(templatePath, "utf-8");
7222
7620
  for (const [key, value] of Object.entries(replacements)) {
7223
7621
  content = content.replaceAll(key, value);
7224
7622
  }
7225
- fs18.writeFileSync(targetPath, content);
7623
+ fs20.writeFileSync(targetPath, content);
7226
7624
  console.log(` Created: ${targetPath} (${resolvedSource})`);
7227
7625
  return { created: true, source: resolvedSource };
7228
7626
  }
7229
7627
  function addToGitignore(cwd) {
7230
- const gitignorePath = path18.join(cwd, ".gitignore");
7628
+ const gitignorePath = path20.join(cwd, ".gitignore");
7231
7629
  const entries = [
7232
7630
  {
7233
7631
  pattern: "/logs/",
@@ -7241,13 +7639,13 @@ function addToGitignore(cwd) {
7241
7639
  },
7242
7640
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
7243
7641
  ];
7244
- if (!fs18.existsSync(gitignorePath)) {
7642
+ if (!fs20.existsSync(gitignorePath)) {
7245
7643
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
7246
- fs18.writeFileSync(gitignorePath, lines.join("\n"));
7644
+ fs20.writeFileSync(gitignorePath, lines.join("\n"));
7247
7645
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
7248
7646
  return;
7249
7647
  }
7250
- const content = fs18.readFileSync(gitignorePath, "utf-8");
7648
+ const content = fs20.readFileSync(gitignorePath, "utf-8");
7251
7649
  const missing = entries.filter((e) => !e.check(content));
7252
7650
  if (missing.length === 0) {
7253
7651
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -7255,7 +7653,7 @@ function addToGitignore(cwd) {
7255
7653
  }
7256
7654
  const additions = missing.map((e) => e.pattern).join("\n");
7257
7655
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
7258
- fs18.writeFileSync(gitignorePath, newContent);
7656
+ fs20.writeFileSync(gitignorePath, newContent);
7259
7657
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7260
7658
  }
7261
7659
  function initCommand(program2) {
@@ -7376,28 +7774,28 @@ function initCommand(program2) {
7376
7774
  "${DEFAULT_BRANCH}": defaultBranch
7377
7775
  };
7378
7776
  step(6, totalSteps, "Creating PRD directory structure...");
7379
- const prdDirPath = path18.join(cwd, prdDir);
7380
- const doneDirPath = path18.join(prdDirPath, "done");
7777
+ const prdDirPath = path20.join(cwd, prdDir);
7778
+ const doneDirPath = path20.join(prdDirPath, "done");
7381
7779
  ensureDir(doneDirPath);
7382
7780
  success(`Created ${prdDirPath}/`);
7383
7781
  success(`Created ${doneDirPath}/`);
7384
7782
  step(7, totalSteps, "Creating logs directory...");
7385
- const logsPath = path18.join(cwd, LOG_DIR);
7783
+ const logsPath = path20.join(cwd, LOG_DIR);
7386
7784
  ensureDir(logsPath);
7387
7785
  success(`Created ${logsPath}/`);
7388
7786
  addToGitignore(cwd);
7389
7787
  step(8, totalSteps, "Creating instructions directory...");
7390
- const instructionsDir = path18.join(cwd, "instructions");
7788
+ const instructionsDir = path20.join(cwd, "instructions");
7391
7789
  ensureDir(instructionsDir);
7392
7790
  success(`Created ${instructionsDir}/`);
7393
7791
  const existingConfig = loadConfig(cwd);
7394
- const customTemplatesDirPath = path18.join(cwd, existingConfig.templatesDir);
7395
- const customTemplatesDir = fs18.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7792
+ const customTemplatesDirPath = path20.join(cwd, existingConfig.templatesDir);
7793
+ const customTemplatesDir = fs20.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7396
7794
  const templateSources = [];
7397
7795
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
7398
7796
  const nwResult = processTemplate(
7399
7797
  "executor.md",
7400
- path18.join(instructionsDir, "executor.md"),
7798
+ path20.join(instructionsDir, "executor.md"),
7401
7799
  replacements,
7402
7800
  force,
7403
7801
  nwResolution.path,
@@ -7411,7 +7809,7 @@ function initCommand(program2) {
7411
7809
  );
7412
7810
  const peResult = processTemplate(
7413
7811
  "prd-executor.md",
7414
- path18.join(instructionsDir, "prd-executor.md"),
7812
+ path20.join(instructionsDir, "prd-executor.md"),
7415
7813
  replacements,
7416
7814
  force,
7417
7815
  peResolution.path,
@@ -7421,7 +7819,7 @@ function initCommand(program2) {
7421
7819
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
7422
7820
  const prResult = processTemplate(
7423
7821
  "pr-reviewer.md",
7424
- path18.join(instructionsDir, "pr-reviewer.md"),
7822
+ path20.join(instructionsDir, "pr-reviewer.md"),
7425
7823
  replacements,
7426
7824
  force,
7427
7825
  prResolution.path,
@@ -7431,7 +7829,7 @@ function initCommand(program2) {
7431
7829
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
7432
7830
  const qaResult = processTemplate(
7433
7831
  "qa.md",
7434
- path18.join(instructionsDir, "qa.md"),
7832
+ path20.join(instructionsDir, "qa.md"),
7435
7833
  replacements,
7436
7834
  force,
7437
7835
  qaResolution.path,
@@ -7441,7 +7839,7 @@ function initCommand(program2) {
7441
7839
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
7442
7840
  const auditResult = processTemplate(
7443
7841
  "audit.md",
7444
- path18.join(instructionsDir, "audit.md"),
7842
+ path20.join(instructionsDir, "audit.md"),
7445
7843
  replacements,
7446
7844
  force,
7447
7845
  auditResolution.path,
@@ -7449,8 +7847,8 @@ function initCommand(program2) {
7449
7847
  );
7450
7848
  templateSources.push({ name: "audit.md", source: auditResult.source });
7451
7849
  step(9, totalSteps, "Creating configuration file...");
7452
- const configPath = path18.join(cwd, CONFIG_FILE_NAME);
7453
- if (fs18.existsSync(configPath) && !force) {
7850
+ const configPath = path20.join(cwd, CONFIG_FILE_NAME);
7851
+ if (fs20.existsSync(configPath) && !force) {
7454
7852
  console.log(` Skipped (exists): ${configPath}`);
7455
7853
  } else {
7456
7854
  const config = buildInitConfig({
@@ -7460,11 +7858,11 @@ function initCommand(program2) {
7460
7858
  reviewerEnabled,
7461
7859
  prdDir
7462
7860
  });
7463
- fs18.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7861
+ fs20.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7464
7862
  success(`Created ${configPath}`);
7465
7863
  }
7466
7864
  step(10, totalSteps, "Setting up GitHub Project board...");
7467
- const existingRaw = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7865
+ const existingRaw = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
7468
7866
  const existingBoard = existingRaw.boardProvider;
7469
7867
  let boardSetupStatus = "Skipped";
7470
7868
  if (existingBoard?.projectNumber && !force) {
@@ -7486,13 +7884,13 @@ function initCommand(program2) {
7486
7884
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
7487
7885
  const boardTitle = `${projectName} Night Watch`;
7488
7886
  const board = await provider.setupBoard(boardTitle);
7489
- const rawConfig = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7887
+ const rawConfig = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
7490
7888
  rawConfig.boardProvider = {
7491
7889
  enabled: true,
7492
7890
  provider: "github",
7493
7891
  projectNumber: board.number
7494
7892
  };
7495
- fs18.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7893
+ fs20.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7496
7894
  boardSetupStatus = `Created (#${board.number})`;
7497
7895
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
7498
7896
  } catch (boardErr) {
@@ -7616,8 +8014,8 @@ function getTelegramStatusWebhooks(config) {
7616
8014
  }
7617
8015
 
7618
8016
  // src/commands/run.ts
7619
- import * as fs19 from "fs";
7620
- import * as path19 from "path";
8017
+ import * as fs21 from "fs";
8018
+ import * as path21 from "path";
7621
8019
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
7622
8020
  if (exitCode === 124) {
7623
8021
  return "run_timeout";
@@ -7649,12 +8047,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7649
8047
  return scriptStatus === "skip_no_eligible_prd";
7650
8048
  }
7651
8049
  function getCrossProjectFallbackCandidates(currentProjectDir) {
7652
- const current = path19.resolve(currentProjectDir);
8050
+ const current = path21.resolve(currentProjectDir);
7653
8051
  const { valid, invalid } = validateRegistry();
7654
8052
  for (const entry of invalid) {
7655
8053
  warn(`Skipping invalid registry entry: ${entry.path}`);
7656
8054
  }
7657
- return valid.filter((entry) => path19.resolve(entry.path) !== current);
8055
+ return valid.filter((entry) => path21.resolve(entry.path) !== current);
7658
8056
  }
7659
8057
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
7660
8058
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -7664,7 +8062,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7664
8062
  if (nonTelegramWebhooks.length > 0) {
7665
8063
  const _rateLimitCtx = {
7666
8064
  event: "rate_limit_fallback",
7667
- projectName: path19.basename(projectDir),
8065
+ projectName: path21.basename(projectDir),
7668
8066
  exitCode,
7669
8067
  provider: config.provider
7670
8068
  };
@@ -7692,7 +8090,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7692
8090
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
7693
8091
  const _ctx = {
7694
8092
  event,
7695
- projectName: path19.basename(projectDir),
8093
+ projectName: path21.basename(projectDir),
7696
8094
  exitCode,
7697
8095
  provider: config.provider,
7698
8096
  prdName: scriptResult?.data.prd,
@@ -7854,20 +8252,20 @@ function applyCliOverrides(config, options) {
7854
8252
  return overridden;
7855
8253
  }
7856
8254
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7857
- const absolutePrdDir = path19.join(projectDir, prdDir);
7858
- const doneDir = path19.join(absolutePrdDir, "done");
8255
+ const absolutePrdDir = path21.join(projectDir, prdDir);
8256
+ const doneDir = path21.join(absolutePrdDir, "done");
7859
8257
  const pending = [];
7860
8258
  const completed = [];
7861
- if (fs19.existsSync(absolutePrdDir)) {
7862
- const entries = fs19.readdirSync(absolutePrdDir, { withFileTypes: true });
8259
+ if (fs21.existsSync(absolutePrdDir)) {
8260
+ const entries = fs21.readdirSync(absolutePrdDir, { withFileTypes: true });
7863
8261
  for (const entry of entries) {
7864
8262
  if (entry.isFile() && entry.name.endsWith(".md")) {
7865
- const claimPath = path19.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
8263
+ const claimPath = path21.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
7866
8264
  let claimed = false;
7867
8265
  let claimInfo = null;
7868
- if (fs19.existsSync(claimPath)) {
8266
+ if (fs21.existsSync(claimPath)) {
7869
8267
  try {
7870
- const content = fs19.readFileSync(claimPath, "utf-8");
8268
+ const content = fs21.readFileSync(claimPath, "utf-8");
7871
8269
  const data = JSON.parse(content);
7872
8270
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
7873
8271
  if (age < maxRuntime) {
@@ -7881,8 +8279,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7881
8279
  }
7882
8280
  }
7883
8281
  }
7884
- if (fs19.existsSync(doneDir)) {
7885
- const entries = fs19.readdirSync(doneDir, { withFileTypes: true });
8282
+ if (fs21.existsSync(doneDir)) {
8283
+ const entries = fs21.readdirSync(doneDir, { withFileTypes: true });
7886
8284
  for (const entry of entries) {
7887
8285
  if (entry.isFile() && entry.name.endsWith(".md")) {
7888
8286
  completed.push(entry.name);
@@ -8042,7 +8440,7 @@ ${stderr}`);
8042
8440
  // src/commands/review.ts
8043
8441
  init_dist();
8044
8442
  import { execFileSync as execFileSync5 } from "child_process";
8045
- import * as path20 from "path";
8443
+ import * as path22 from "path";
8046
8444
  function shouldSendReviewNotification(scriptStatus) {
8047
8445
  if (!scriptStatus) {
8048
8446
  return true;
@@ -8282,7 +8680,7 @@ ${stderr}`);
8282
8680
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
8283
8681
  const _reviewCtx = {
8284
8682
  event: "review_completed",
8285
- projectName: path20.basename(projectDir),
8683
+ projectName: path22.basename(projectDir),
8286
8684
  exitCode,
8287
8685
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8288
8686
  prUrl: prDetails?.url,
@@ -8303,7 +8701,7 @@ ${stderr}`);
8303
8701
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
8304
8702
  const _mergeCtx = {
8305
8703
  event: "pr_auto_merged",
8306
- projectName: path20.basename(projectDir),
8704
+ projectName: path22.basename(projectDir),
8307
8705
  exitCode,
8308
8706
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8309
8707
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -8328,7 +8726,7 @@ ${stderr}`);
8328
8726
 
8329
8727
  // src/commands/qa.ts
8330
8728
  init_dist();
8331
- import * as path21 from "path";
8729
+ import * as path23 from "path";
8332
8730
  function shouldSendQaNotification(scriptStatus) {
8333
8731
  if (!scriptStatus) {
8334
8732
  return true;
@@ -8462,7 +8860,7 @@ ${stderr}`);
8462
8860
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
8463
8861
  const _qaCtx = {
8464
8862
  event: "qa_completed",
8465
- projectName: path21.basename(projectDir),
8863
+ projectName: path23.basename(projectDir),
8466
8864
  exitCode,
8467
8865
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8468
8866
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -8488,8 +8886,8 @@ ${stderr}`);
8488
8886
 
8489
8887
  // src/commands/audit.ts
8490
8888
  init_dist();
8491
- import * as fs20 from "fs";
8492
- import * as path22 from "path";
8889
+ import * as fs22 from "fs";
8890
+ import * as path24 from "path";
8493
8891
  function buildEnvVars4(config, options) {
8494
8892
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
8495
8893
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -8532,7 +8930,7 @@ function auditCommand(program2) {
8532
8930
  configTable.push(["Provider", auditProvider]);
8533
8931
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
8534
8932
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
8535
- configTable.push(["Report File", path22.join(projectDir, "logs", "audit-report.md")]);
8933
+ configTable.push(["Report File", path24.join(projectDir, "logs", "audit-report.md")]);
8536
8934
  console.log(configTable.toString());
8537
8935
  header("Provider Invocation");
8538
8936
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -8567,8 +8965,8 @@ ${stderr}`);
8567
8965
  } else if (scriptResult?.status?.startsWith("skip_")) {
8568
8966
  spinner.succeed("Code audit skipped");
8569
8967
  } else {
8570
- const reportPath = path22.join(projectDir, "logs", "audit-report.md");
8571
- if (!fs20.existsSync(reportPath)) {
8968
+ const reportPath = path24.join(projectDir, "logs", "audit-report.md");
8969
+ if (!fs22.existsSync(reportPath)) {
8572
8970
  spinner.fail("Code audit finished without a report file");
8573
8971
  process.exit(1);
8574
8972
  }
@@ -8579,9 +8977,9 @@ ${stderr}`);
8579
8977
  const providerExit = scriptResult?.data?.provider_exit;
8580
8978
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
8581
8979
  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);
8980
+ const logPath = path24.join(projectDir, "logs", "audit.log");
8981
+ if (fs22.existsSync(logPath)) {
8982
+ const logLines = fs22.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
8585
8983
  if (logLines.length > 0) {
8586
8984
  process.stderr.write(logLines.join("\n") + "\n");
8587
8985
  }
@@ -8595,19 +8993,80 @@ ${stderr}`);
8595
8993
  });
8596
8994
  }
8597
8995
 
8996
+ // src/commands/analytics.ts
8997
+ init_dist();
8998
+ function analyticsCommand(program2) {
8999
+ 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) => {
9000
+ const projectDir = process.cwd();
9001
+ let config = loadConfig(projectDir);
9002
+ if (options.timeout) {
9003
+ const timeout = parseInt(options.timeout, 10);
9004
+ if (!isNaN(timeout)) {
9005
+ config = { ...config, analytics: { ...config.analytics, maxRuntime: timeout } };
9006
+ }
9007
+ }
9008
+ if (options.provider) {
9009
+ config = {
9010
+ ...config,
9011
+ _cliProviderOverride: options.provider
9012
+ };
9013
+ }
9014
+ if (!config.analytics.enabled && !options.dryRun) {
9015
+ info("Analytics is disabled in config; skipping run.");
9016
+ process.exit(0);
9017
+ }
9018
+ const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
9019
+ const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
9020
+ if (!apiKey || !secretKey) {
9021
+ info(
9022
+ "AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
9023
+ );
9024
+ process.exit(1);
9025
+ }
9026
+ if (options.dryRun) {
9027
+ header("Dry Run: Analytics Job");
9028
+ const analyticsProvider = resolveJobProvider(config, "analytics");
9029
+ header("Configuration");
9030
+ const configTable = createTable({ head: ["Setting", "Value"] });
9031
+ configTable.push(["Provider", analyticsProvider]);
9032
+ configTable.push(["Max Runtime", `${config.analytics.maxRuntime}s`]);
9033
+ configTable.push(["Lookback Days", String(config.analytics.lookbackDays)]);
9034
+ configTable.push(["Target Column", config.analytics.targetColumn]);
9035
+ configTable.push(["Amplitude API Key", apiKey ? "***" + apiKey.slice(-4) : "not set"]);
9036
+ console.log(configTable.toString());
9037
+ console.log();
9038
+ process.exit(0);
9039
+ }
9040
+ const spinner = createSpinner("Running analytics job...");
9041
+ spinner.start();
9042
+ try {
9043
+ await maybeApplyCronSchedulingDelay(config, "analytics", projectDir);
9044
+ const result = await runAnalytics(config, projectDir);
9045
+ if (result.issuesCreated > 0) {
9046
+ spinner.succeed(`Analytics complete \u2014 ${result.summary}`);
9047
+ } else {
9048
+ spinner.succeed("Analytics complete \u2014 no actionable insights found");
9049
+ }
9050
+ } catch (err) {
9051
+ spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`);
9052
+ process.exit(1);
9053
+ }
9054
+ });
9055
+ }
9056
+
8598
9057
  // src/commands/install.ts
8599
9058
  init_dist();
8600
9059
  import { execSync as execSync4 } from "child_process";
8601
- import * as path23 from "path";
8602
- import * as fs21 from "fs";
9060
+ import * as path25 from "path";
9061
+ import * as fs23 from "fs";
8603
9062
  function shellQuote(value) {
8604
9063
  return `'${value.replace(/'/g, `'"'"'`)}'`;
8605
9064
  }
8606
9065
  function getNightWatchBinPath() {
8607
9066
  try {
8608
9067
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
8609
- const binPath = path23.join(npmBin, "night-watch");
8610
- if (fs21.existsSync(binPath)) {
9068
+ const binPath = path25.join(npmBin, "night-watch");
9069
+ if (fs23.existsSync(binPath)) {
8611
9070
  return binPath;
8612
9071
  }
8613
9072
  } catch {
@@ -8620,17 +9079,17 @@ function getNightWatchBinPath() {
8620
9079
  }
8621
9080
  function getNodeBinDir() {
8622
9081
  if (process.execPath && process.execPath !== "node") {
8623
- return path23.dirname(process.execPath);
9082
+ return path25.dirname(process.execPath);
8624
9083
  }
8625
9084
  try {
8626
9085
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
8627
- return path23.dirname(nodePath);
9086
+ return path25.dirname(nodePath);
8628
9087
  } catch {
8629
9088
  return "";
8630
9089
  }
8631
9090
  }
8632
9091
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
8633
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path23.dirname(nightWatchBin) : "";
9092
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path25.dirname(nightWatchBin) : "";
8634
9093
  const pathParts = Array.from(
8635
9094
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
8636
9095
  );
@@ -8646,12 +9105,12 @@ function performInstall(projectDir, config, options) {
8646
9105
  const nightWatchBin = getNightWatchBinPath();
8647
9106
  const projectName = getProjectName(projectDir);
8648
9107
  const marker = generateMarker(projectName);
8649
- const logDir = path23.join(projectDir, LOG_DIR);
8650
- if (!fs21.existsSync(logDir)) {
8651
- fs21.mkdirSync(logDir, { recursive: true });
9108
+ const logDir = path25.join(projectDir, LOG_DIR);
9109
+ if (!fs23.existsSync(logDir)) {
9110
+ fs23.mkdirSync(logDir, { recursive: true });
8652
9111
  }
8653
- const executorLog = path23.join(logDir, "executor.log");
8654
- const reviewerLog = path23.join(logDir, "reviewer.log");
9112
+ const executorLog = path25.join(logDir, "executor.log");
9113
+ const reviewerLog = path25.join(logDir, "reviewer.log");
8655
9114
  if (!options?.force) {
8656
9115
  const existingEntries2 = Array.from(
8657
9116
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -8688,7 +9147,7 @@ function performInstall(projectDir, config, options) {
8688
9147
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
8689
9148
  if (installSlicer) {
8690
9149
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8691
- const slicerLog = path23.join(logDir, "slicer.log");
9150
+ const slicerLog = path25.join(logDir, "slicer.log");
8692
9151
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8693
9152
  entries.push(slicerEntry);
8694
9153
  }
@@ -8696,7 +9155,7 @@ function performInstall(projectDir, config, options) {
8696
9155
  const installQa = disableQa ? false : config.qa.enabled;
8697
9156
  if (installQa) {
8698
9157
  const qaSchedule = config.qa.schedule;
8699
- const qaLog = path23.join(logDir, "qa.log");
9158
+ const qaLog = path25.join(logDir, "qa.log");
8700
9159
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8701
9160
  entries.push(qaEntry);
8702
9161
  }
@@ -8704,10 +9163,18 @@ function performInstall(projectDir, config, options) {
8704
9163
  const installAudit = disableAudit ? false : config.audit.enabled;
8705
9164
  if (installAudit) {
8706
9165
  const auditSchedule = config.audit.schedule;
8707
- const auditLog = path23.join(logDir, "audit.log");
9166
+ const auditLog = path25.join(logDir, "audit.log");
8708
9167
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8709
9168
  entries.push(auditEntry);
8710
9169
  }
9170
+ const disableAnalytics = options?.noAnalytics === true || options?.analytics === false;
9171
+ const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9172
+ if (installAnalytics) {
9173
+ const analyticsSchedule = config.analytics.schedule;
9174
+ const analyticsLog = path25.join(logDir, "analytics.log");
9175
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9176
+ entries.push(analyticsEntry);
9177
+ }
8711
9178
  const existingEntries = new Set(
8712
9179
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
8713
9180
  );
@@ -8726,7 +9193,7 @@ function performInstall(projectDir, config, options) {
8726
9193
  }
8727
9194
  }
8728
9195
  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) => {
9196
+ 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
9197
  try {
8731
9198
  const projectDir = process.cwd();
8732
9199
  const config = loadConfig(projectDir);
@@ -8735,12 +9202,12 @@ function installCommand(program2) {
8735
9202
  const nightWatchBin = getNightWatchBinPath();
8736
9203
  const projectName = getProjectName(projectDir);
8737
9204
  const marker = generateMarker(projectName);
8738
- const logDir = path23.join(projectDir, LOG_DIR);
8739
- if (!fs21.existsSync(logDir)) {
8740
- fs21.mkdirSync(logDir, { recursive: true });
9205
+ const logDir = path25.join(projectDir, LOG_DIR);
9206
+ if (!fs23.existsSync(logDir)) {
9207
+ fs23.mkdirSync(logDir, { recursive: true });
8741
9208
  }
8742
- const executorLog = path23.join(logDir, "executor.log");
8743
- const reviewerLog = path23.join(logDir, "reviewer.log");
9209
+ const executorLog = path25.join(logDir, "executor.log");
9210
+ const reviewerLog = path25.join(logDir, "reviewer.log");
8744
9211
  const existingEntries = Array.from(
8745
9212
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
8746
9213
  );
@@ -8776,7 +9243,7 @@ function installCommand(program2) {
8776
9243
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
8777
9244
  let slicerLog;
8778
9245
  if (installSlicer) {
8779
- slicerLog = path23.join(logDir, "slicer.log");
9246
+ slicerLog = path25.join(logDir, "slicer.log");
8780
9247
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8781
9248
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8782
9249
  entries.push(slicerEntry);
@@ -8785,7 +9252,7 @@ function installCommand(program2) {
8785
9252
  const installQa = disableQa ? false : config.qa.enabled;
8786
9253
  let qaLog;
8787
9254
  if (installQa) {
8788
- qaLog = path23.join(logDir, "qa.log");
9255
+ qaLog = path25.join(logDir, "qa.log");
8789
9256
  const qaSchedule = config.qa.schedule;
8790
9257
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8791
9258
  entries.push(qaEntry);
@@ -8794,11 +9261,20 @@ function installCommand(program2) {
8794
9261
  const installAudit = disableAudit ? false : config.audit.enabled;
8795
9262
  let auditLog;
8796
9263
  if (installAudit) {
8797
- auditLog = path23.join(logDir, "audit.log");
9264
+ auditLog = path25.join(logDir, "audit.log");
8798
9265
  const auditSchedule = config.audit.schedule;
8799
9266
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8800
9267
  entries.push(auditEntry);
8801
9268
  }
9269
+ const disableAnalytics = options.noAnalytics === true || options.analytics === false;
9270
+ const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9271
+ let analyticsLog;
9272
+ if (installAnalytics) {
9273
+ analyticsLog = path25.join(logDir, "analytics.log");
9274
+ const analyticsSchedule = config.analytics.schedule;
9275
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9276
+ entries.push(analyticsEntry);
9277
+ }
8802
9278
  const existingEntrySet = new Set(existingEntries);
8803
9279
  const currentCrontab = readCrontab();
8804
9280
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -8825,6 +9301,9 @@ function installCommand(program2) {
8825
9301
  if (installAudit && auditLog) {
8826
9302
  dim(` Audit: ${auditLog}`);
8827
9303
  }
9304
+ if (installAnalytics && analyticsLog) {
9305
+ dim(` Analytics: ${analyticsLog}`);
9306
+ }
8828
9307
  console.log();
8829
9308
  dim("To uninstall, run: night-watch uninstall");
8830
9309
  dim("To check status, run: night-watch status");
@@ -8839,8 +9318,8 @@ function installCommand(program2) {
8839
9318
 
8840
9319
  // src/commands/uninstall.ts
8841
9320
  init_dist();
8842
- import * as path24 from "path";
8843
- import * as fs22 from "fs";
9321
+ import * as path26 from "path";
9322
+ import * as fs24 from "fs";
8844
9323
  function performUninstall(projectDir, options) {
8845
9324
  try {
8846
9325
  const projectName = getProjectName(projectDir);
@@ -8855,19 +9334,19 @@ function performUninstall(projectDir, options) {
8855
9334
  const removedCount = removeEntriesForProject(projectDir, marker);
8856
9335
  unregisterProject(projectDir);
8857
9336
  if (!options?.keepLogs) {
8858
- const logDir = path24.join(projectDir, "logs");
8859
- if (fs22.existsSync(logDir)) {
9337
+ const logDir = path26.join(projectDir, "logs");
9338
+ if (fs24.existsSync(logDir)) {
8860
9339
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8861
9340
  logFiles.forEach((logFile) => {
8862
- const logPath = path24.join(logDir, logFile);
8863
- if (fs22.existsSync(logPath)) {
8864
- fs22.unlinkSync(logPath);
9341
+ const logPath = path26.join(logDir, logFile);
9342
+ if (fs24.existsSync(logPath)) {
9343
+ fs24.unlinkSync(logPath);
8865
9344
  }
8866
9345
  });
8867
9346
  try {
8868
- const remainingFiles = fs22.readdirSync(logDir);
9347
+ const remainingFiles = fs24.readdirSync(logDir);
8869
9348
  if (remainingFiles.length === 0) {
8870
- fs22.rmdirSync(logDir);
9349
+ fs24.rmdirSync(logDir);
8871
9350
  }
8872
9351
  } catch {
8873
9352
  }
@@ -8900,21 +9379,21 @@ function uninstallCommand(program2) {
8900
9379
  existingEntries.forEach((entry) => dim(` ${entry}`));
8901
9380
  const removedCount = removeEntriesForProject(projectDir, marker);
8902
9381
  if (!options.keepLogs) {
8903
- const logDir = path24.join(projectDir, "logs");
8904
- if (fs22.existsSync(logDir)) {
9382
+ const logDir = path26.join(projectDir, "logs");
9383
+ if (fs24.existsSync(logDir)) {
8905
9384
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8906
9385
  let logsRemoved = 0;
8907
9386
  logFiles.forEach((logFile) => {
8908
- const logPath = path24.join(logDir, logFile);
8909
- if (fs22.existsSync(logPath)) {
8910
- fs22.unlinkSync(logPath);
9387
+ const logPath = path26.join(logDir, logFile);
9388
+ if (fs24.existsSync(logPath)) {
9389
+ fs24.unlinkSync(logPath);
8911
9390
  logsRemoved++;
8912
9391
  }
8913
9392
  });
8914
9393
  try {
8915
- const remainingFiles = fs22.readdirSync(logDir);
9394
+ const remainingFiles = fs24.readdirSync(logDir);
8916
9395
  if (remainingFiles.length === 0) {
8917
- fs22.rmdirSync(logDir);
9396
+ fs24.rmdirSync(logDir);
8918
9397
  }
8919
9398
  } catch {
8920
9399
  }
@@ -9150,14 +9629,14 @@ function statusCommand(program2) {
9150
9629
  // src/commands/logs.ts
9151
9630
  init_dist();
9152
9631
  import { spawn as spawn3 } from "child_process";
9153
- import * as path25 from "path";
9154
- import * as fs23 from "fs";
9632
+ import * as path27 from "path";
9633
+ import * as fs25 from "fs";
9155
9634
  function getLastLines(filePath, lineCount) {
9156
- if (!fs23.existsSync(filePath)) {
9635
+ if (!fs25.existsSync(filePath)) {
9157
9636
  return `Log file not found: ${filePath}`;
9158
9637
  }
9159
9638
  try {
9160
- const content = fs23.readFileSync(filePath, "utf-8");
9639
+ const content = fs25.readFileSync(filePath, "utf-8");
9161
9640
  const lines = content.trim().split("\n");
9162
9641
  return lines.slice(-lineCount).join("\n");
9163
9642
  } catch (error2) {
@@ -9165,7 +9644,7 @@ function getLastLines(filePath, lineCount) {
9165
9644
  }
9166
9645
  }
9167
9646
  function followLog(filePath) {
9168
- if (!fs23.existsSync(filePath)) {
9647
+ if (!fs25.existsSync(filePath)) {
9169
9648
  console.log(`Log file not found: ${filePath}`);
9170
9649
  console.log("The log file will be created when the first execution runs.");
9171
9650
  return;
@@ -9185,13 +9664,13 @@ function logsCommand(program2) {
9185
9664
  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
9665
  try {
9187
9666
  const projectDir = process.cwd();
9188
- const logDir = path25.join(projectDir, LOG_DIR);
9667
+ const logDir = path27.join(projectDir, LOG_DIR);
9189
9668
  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`);
9669
+ const executorLog = path27.join(logDir, EXECUTOR_LOG_FILE);
9670
+ const reviewerLog = path27.join(logDir, REVIEWER_LOG_FILE);
9671
+ const qaLog = path27.join(logDir, `${QA_LOG_NAME}.log`);
9672
+ const auditLog = path27.join(logDir, `${AUDIT_LOG_NAME}.log`);
9673
+ const plannerLog = path27.join(logDir, `${PLANNER_LOG_NAME}.log`);
9195
9674
  const logType = options.type?.toLowerCase() || "all";
9196
9675
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
9197
9676
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -9255,15 +9734,15 @@ function logsCommand(program2) {
9255
9734
 
9256
9735
  // src/commands/prd.ts
9257
9736
  init_dist();
9258
- import * as fs24 from "fs";
9259
- import * as path26 from "path";
9737
+ import * as fs26 from "fs";
9738
+ import * as path28 from "path";
9260
9739
  import * as readline2 from "readline";
9261
9740
  function slugify2(name) {
9262
9741
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9263
9742
  }
9264
9743
  function getNextPrdNumber2(prdDir) {
9265
- if (!fs24.existsSync(prdDir)) return 1;
9266
- const files = fs24.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
9744
+ if (!fs26.existsSync(prdDir)) return 1;
9745
+ const files = fs26.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
9267
9746
  const numbers = files.map((f) => {
9268
9747
  const match = f.match(/^(\d+)-/);
9269
9748
  return match ? parseInt(match[1], 10) : 0;
@@ -9284,10 +9763,10 @@ function parseDependencies(content) {
9284
9763
  }
9285
9764
  function isClaimActive(claimPath, maxRuntime) {
9286
9765
  try {
9287
- if (!fs24.existsSync(claimPath)) {
9766
+ if (!fs26.existsSync(claimPath)) {
9288
9767
  return { active: false };
9289
9768
  }
9290
- const content = fs24.readFileSync(claimPath, "utf-8");
9769
+ const content = fs26.readFileSync(claimPath, "utf-8");
9291
9770
  const claim = JSON.parse(content);
9292
9771
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
9293
9772
  if (age < maxRuntime) {
@@ -9303,9 +9782,9 @@ function prdCommand(program2) {
9303
9782
  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
9783
  const projectDir = process.cwd();
9305
9784
  const config = loadConfig(projectDir);
9306
- const prdDir = path26.join(projectDir, config.prdDir);
9307
- if (!fs24.existsSync(prdDir)) {
9308
- fs24.mkdirSync(prdDir, { recursive: true });
9785
+ const prdDir = path28.join(projectDir, config.prdDir);
9786
+ if (!fs26.existsSync(prdDir)) {
9787
+ fs26.mkdirSync(prdDir, { recursive: true });
9309
9788
  }
9310
9789
  let complexityScore = 5;
9311
9790
  let dependsOn = [];
@@ -9364,20 +9843,20 @@ function prdCommand(program2) {
9364
9843
  } else {
9365
9844
  filename = `${slug}.md`;
9366
9845
  }
9367
- const filePath = path26.join(prdDir, filename);
9368
- if (fs24.existsSync(filePath)) {
9846
+ const filePath = path28.join(prdDir, filename);
9847
+ if (fs26.existsSync(filePath)) {
9369
9848
  error(`File already exists: ${filePath}`);
9370
9849
  dim("Use a different name or remove the existing file.");
9371
9850
  process.exit(1);
9372
9851
  }
9373
9852
  let customTemplate;
9374
9853
  if (options.template) {
9375
- const templatePath = path26.resolve(options.template);
9376
- if (!fs24.existsSync(templatePath)) {
9854
+ const templatePath = path28.resolve(options.template);
9855
+ if (!fs26.existsSync(templatePath)) {
9377
9856
  error(`Template file not found: ${templatePath}`);
9378
9857
  process.exit(1);
9379
9858
  }
9380
- customTemplate = fs24.readFileSync(templatePath, "utf-8");
9859
+ customTemplate = fs26.readFileSync(templatePath, "utf-8");
9381
9860
  }
9382
9861
  const vars = {
9383
9862
  title: name,
@@ -9388,7 +9867,7 @@ function prdCommand(program2) {
9388
9867
  phaseCount
9389
9868
  };
9390
9869
  const content = renderPrdTemplate(vars, customTemplate);
9391
- fs24.writeFileSync(filePath, content, "utf-8");
9870
+ fs26.writeFileSync(filePath, content, "utf-8");
9392
9871
  header("PRD Created");
9393
9872
  success(`Created: ${filePath}`);
9394
9873
  info(`Title: ${name}`);
@@ -9400,15 +9879,15 @@ function prdCommand(program2) {
9400
9879
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
9401
9880
  const projectDir = process.cwd();
9402
9881
  const config = loadConfig(projectDir);
9403
- const absolutePrdDir = path26.join(projectDir, config.prdDir);
9404
- const doneDir = path26.join(absolutePrdDir, "done");
9882
+ const absolutePrdDir = path28.join(projectDir, config.prdDir);
9883
+ const doneDir = path28.join(absolutePrdDir, "done");
9405
9884
  const pending = [];
9406
- if (fs24.existsSync(absolutePrdDir)) {
9407
- const files = fs24.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
9885
+ if (fs26.existsSync(absolutePrdDir)) {
9886
+ const files = fs26.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
9408
9887
  for (const file of files) {
9409
- const content = fs24.readFileSync(path26.join(absolutePrdDir, file), "utf-8");
9888
+ const content = fs26.readFileSync(path28.join(absolutePrdDir, file), "utf-8");
9410
9889
  const deps = parseDependencies(content);
9411
- const claimPath = path26.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9890
+ const claimPath = path28.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9412
9891
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
9413
9892
  pending.push({
9414
9893
  name: file,
@@ -9419,10 +9898,10 @@ function prdCommand(program2) {
9419
9898
  }
9420
9899
  }
9421
9900
  const done = [];
9422
- if (fs24.existsSync(doneDir)) {
9423
- const files = fs24.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9901
+ if (fs26.existsSync(doneDir)) {
9902
+ const files = fs26.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9424
9903
  for (const file of files) {
9425
- const content = fs24.readFileSync(path26.join(doneDir, file), "utf-8");
9904
+ const content = fs26.readFileSync(path28.join(doneDir, file), "utf-8");
9426
9905
  const deps = parseDependencies(content);
9427
9906
  done.push({ name: file, dependencies: deps });
9428
9907
  }
@@ -9461,7 +9940,7 @@ import blessed6 from "blessed";
9461
9940
  // src/commands/dashboard/tab-status.ts
9462
9941
  init_dist();
9463
9942
  import blessed from "blessed";
9464
- import * as fs25 from "fs";
9943
+ import * as fs27 from "fs";
9465
9944
  function sortPrdsByPriority(prds, priority) {
9466
9945
  if (priority.length === 0) return prds;
9467
9946
  const priorityMap = /* @__PURE__ */ new Map();
@@ -9557,7 +10036,7 @@ function renderLogPane(projectDir, logs) {
9557
10036
  let newestMtime = 0;
9558
10037
  for (const log of existingLogs) {
9559
10038
  try {
9560
- const stat = fs25.statSync(log.path);
10039
+ const stat = fs27.statSync(log.path);
9561
10040
  if (stat.mtimeMs > newestMtime) {
9562
10041
  newestMtime = stat.mtimeMs;
9563
10042
  newestLog = log;
@@ -11212,8 +11691,8 @@ function createActionsTab() {
11212
11691
  // src/commands/dashboard/tab-logs.ts
11213
11692
  init_dist();
11214
11693
  import blessed5 from "blessed";
11215
- import * as fs26 from "fs";
11216
- import * as path27 from "path";
11694
+ import * as fs28 from "fs";
11695
+ import * as path29 from "path";
11217
11696
  var LOG_NAMES = ["executor", "reviewer"];
11218
11697
  var LOG_LINES = 200;
11219
11698
  function createLogsTab() {
@@ -11254,7 +11733,7 @@ function createLogsTab() {
11254
11733
  let activeKeyHandlers = [];
11255
11734
  let activeCtx = null;
11256
11735
  function getLogPath(projectDir, logName) {
11257
- return path27.join(projectDir, "logs", `${logName}.log`);
11736
+ return path29.join(projectDir, "logs", `${logName}.log`);
11258
11737
  }
11259
11738
  function updateSelector() {
11260
11739
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -11268,7 +11747,7 @@ function createLogsTab() {
11268
11747
  function loadLog(ctx) {
11269
11748
  const logName = LOG_NAMES[selectedLogIndex];
11270
11749
  const logPath = getLogPath(ctx.projectDir, logName);
11271
- if (!fs26.existsSync(logPath)) {
11750
+ if (!fs28.existsSync(logPath)) {
11272
11751
  logContent.setContent(
11273
11752
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
11274
11753
 
@@ -11278,7 +11757,7 @@ Log will appear here once the ${logName} runs.`
11278
11757
  return;
11279
11758
  }
11280
11759
  try {
11281
- const stat = fs26.statSync(logPath);
11760
+ const stat = fs28.statSync(logPath);
11282
11761
  const sizeKB = (stat.size / 1024).toFixed(1);
11283
11762
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
11284
11763
  } catch {
@@ -11784,13 +12263,13 @@ function doctorCommand(program2) {
11784
12263
 
11785
12264
  // src/commands/serve.ts
11786
12265
  init_dist();
11787
- import * as fs31 from "fs";
12266
+ import * as fs33 from "fs";
11788
12267
 
11789
12268
  // ../server/dist/index.js
11790
12269
  init_dist();
11791
- import * as fs30 from "fs";
11792
- import * as path33 from "path";
11793
- import { dirname as dirname7 } from "path";
12270
+ import * as fs32 from "fs";
12271
+ import * as path35 from "path";
12272
+ import { dirname as dirname8 } from "path";
11794
12273
  import { fileURLToPath as fileURLToPath3 } from "url";
11795
12274
  import cors from "cors";
11796
12275
  import express from "express";
@@ -11874,8 +12353,8 @@ function setupGracefulShutdown(server, beforeClose) {
11874
12353
 
11875
12354
  // ../server/dist/middleware/project-resolver.middleware.js
11876
12355
  init_dist();
11877
- import * as fs27 from "fs";
11878
- import * as path28 from "path";
12356
+ import * as fs29 from "fs";
12357
+ import * as path30 from "path";
11879
12358
  function resolveProject(req, res, next) {
11880
12359
  const projectId = req.params.projectId;
11881
12360
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -11885,7 +12364,7 @@ function resolveProject(req, res, next) {
11885
12364
  res.status(404).json({ error: `Project not found: ${decodedId}` });
11886
12365
  return;
11887
12366
  }
11888
- if (!fs27.existsSync(entry.path) || !fs27.existsSync(path28.join(entry.path, CONFIG_FILE_NAME))) {
12367
+ if (!fs29.existsSync(entry.path) || !fs29.existsSync(path30.join(entry.path, CONFIG_FILE_NAME))) {
11889
12368
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
11890
12369
  return;
11891
12370
  }
@@ -11930,8 +12409,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
11930
12409
 
11931
12410
  // ../server/dist/routes/action.routes.js
11932
12411
  init_dist();
11933
- import * as fs28 from "fs";
11934
- import * as path29 from "path";
12412
+ import * as fs30 from "fs";
12413
+ import * as path31 from "path";
11935
12414
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
11936
12415
  import { Router } from "express";
11937
12416
 
@@ -11969,17 +12448,17 @@ function getBoardProvider(config, projectDir) {
11969
12448
  function cleanOrphanedClaims(dir) {
11970
12449
  let entries;
11971
12450
  try {
11972
- entries = fs28.readdirSync(dir, { withFileTypes: true });
12451
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
11973
12452
  } catch {
11974
12453
  return;
11975
12454
  }
11976
12455
  for (const entry of entries) {
11977
- const fullPath = path29.join(dir, entry.name);
12456
+ const fullPath = path31.join(dir, entry.name);
11978
12457
  if (entry.isDirectory() && entry.name !== "done") {
11979
12458
  cleanOrphanedClaims(fullPath);
11980
12459
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
11981
12460
  try {
11982
- fs28.unlinkSync(fullPath);
12461
+ fs30.unlinkSync(fullPath);
11983
12462
  } catch {
11984
12463
  }
11985
12464
  }
@@ -12076,6 +12555,9 @@ function createActionRouteHandlers(ctx) {
12076
12555
  router.post(`/${p}audit`, (req, res) => {
12077
12556
  spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12078
12557
  });
12558
+ router.post(`/${p}analytics`, (req, res) => {
12559
+ spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12560
+ });
12079
12561
  router.post(`/${p}planner`, (req, res) => {
12080
12562
  spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12081
12563
  });
@@ -12133,19 +12615,19 @@ function createActionRouteHandlers(ctx) {
12133
12615
  res.status(400).json({ error: "Invalid PRD name" });
12134
12616
  return;
12135
12617
  }
12136
- const prdDir = path29.join(projectDir, config.prdDir);
12618
+ const prdDir = path31.join(projectDir, config.prdDir);
12137
12619
  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)) {
12620
+ const pendingPath = path31.join(prdDir, normalized);
12621
+ const donePath = path31.join(prdDir, "done", normalized);
12622
+ if (fs30.existsSync(pendingPath)) {
12141
12623
  res.json({ message: `"${normalized}" is already pending` });
12142
12624
  return;
12143
12625
  }
12144
- if (!fs28.existsSync(donePath)) {
12626
+ if (!fs30.existsSync(donePath)) {
12145
12627
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
12146
12628
  return;
12147
12629
  }
12148
- fs28.renameSync(donePath, pendingPath);
12630
+ fs30.renameSync(donePath, pendingPath);
12149
12631
  res.json({ message: `Moved "${normalized}" back to pending` });
12150
12632
  } catch (error2) {
12151
12633
  res.status(500).json({
@@ -12163,11 +12645,11 @@ function createActionRouteHandlers(ctx) {
12163
12645
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
12164
12646
  return;
12165
12647
  }
12166
- if (fs28.existsSync(lockPath)) {
12167
- fs28.unlinkSync(lockPath);
12648
+ if (fs30.existsSync(lockPath)) {
12649
+ fs30.unlinkSync(lockPath);
12168
12650
  }
12169
- const prdDir = path29.join(projectDir, config.prdDir);
12170
- if (fs28.existsSync(prdDir)) {
12651
+ const prdDir = path31.join(projectDir, config.prdDir);
12652
+ if (fs30.existsSync(prdDir)) {
12171
12653
  cleanOrphanedClaims(prdDir);
12172
12654
  }
12173
12655
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -12363,6 +12845,7 @@ function createBoardRouteHandlers(ctx) {
12363
12845
  return;
12364
12846
  }
12365
12847
  await provider.closeIssue(issueNumber);
12848
+ await provider.moveIssue(issueNumber, "Done");
12366
12849
  invalidateBoardCache(projectDir);
12367
12850
  res.json({ closed: true });
12368
12851
  } catch (error2) {
@@ -12669,6 +13152,34 @@ function validateConfigChanges(changes, currentConfig) {
12669
13152
  return "audit.maxRuntime must be a number >= 60";
12670
13153
  }
12671
13154
  }
13155
+ if (changes.analytics !== void 0) {
13156
+ if (typeof changes.analytics !== "object" || changes.analytics === null) {
13157
+ return "analytics must be an object";
13158
+ }
13159
+ const analytics = changes.analytics;
13160
+ if (analytics.enabled !== void 0 && typeof analytics.enabled !== "boolean") {
13161
+ return "analytics.enabled must be a boolean";
13162
+ }
13163
+ const analyticsScheduleError = validateCronField("analytics.schedule", analytics.schedule);
13164
+ if (analyticsScheduleError) {
13165
+ return analyticsScheduleError;
13166
+ }
13167
+ if (analytics.maxRuntime !== void 0 && (typeof analytics.maxRuntime !== "number" || analytics.maxRuntime < 60)) {
13168
+ return "analytics.maxRuntime must be a number >= 60";
13169
+ }
13170
+ if (analytics.lookbackDays !== void 0 && (typeof analytics.lookbackDays !== "number" || analytics.lookbackDays < 1 || analytics.lookbackDays > 90)) {
13171
+ return "analytics.lookbackDays must be a number between 1 and 90";
13172
+ }
13173
+ if (analytics.targetColumn !== void 0) {
13174
+ const validColumns = ["Draft", "Ready", "In Progress", "Review", "Done"];
13175
+ if (!validColumns.includes(analytics.targetColumn)) {
13176
+ return `analytics.targetColumn must be one of: ${validColumns.join(", ")}`;
13177
+ }
13178
+ }
13179
+ if (analytics.analysisPrompt !== void 0 && typeof analytics.analysisPrompt !== "string") {
13180
+ return "analytics.analysisPrompt must be a string";
13181
+ }
13182
+ }
12672
13183
  if (changes.queue !== void 0) {
12673
13184
  if (typeof changes.queue !== "object" || changes.queue === null) {
12674
13185
  return "queue must be an object";
@@ -12687,7 +13198,14 @@ function validateConfigChanges(changes, currentConfig) {
12687
13198
  if (typeof queue.priority !== "object" || queue.priority === null) {
12688
13199
  return "queue.priority must be an object";
12689
13200
  }
12690
- const validQueueJobs = ["executor", "reviewer", "qa", "audit", "slicer"];
13201
+ const validQueueJobs = [
13202
+ "executor",
13203
+ "reviewer",
13204
+ "qa",
13205
+ "audit",
13206
+ "slicer",
13207
+ "analytics"
13208
+ ];
12691
13209
  for (const [jobType, value] of Object.entries(queue.priority)) {
12692
13210
  if (!validQueueJobs.includes(jobType)) {
12693
13211
  return `queue.priority contains invalid job type: ${jobType}`;
@@ -12790,6 +13308,26 @@ function createConfigRoutes(deps) {
12790
13308
  });
12791
13309
  return router;
12792
13310
  }
13311
+ function createGlobalNotificationsRoutes() {
13312
+ const router = Router3();
13313
+ router.get("/", (_req, res) => {
13314
+ res.json(loadGlobalNotificationsConfig());
13315
+ });
13316
+ router.put("/", (req, res) => {
13317
+ const { webhook } = req.body;
13318
+ if (webhook !== null && webhook !== void 0) {
13319
+ const issues = validateWebhook(webhook);
13320
+ if (issues.length > 0) {
13321
+ res.status(400).json({ error: `Invalid webhook: ${issues.join(", ")}` });
13322
+ return;
13323
+ }
13324
+ }
13325
+ const config = { webhook: webhook ?? null };
13326
+ saveGlobalNotificationsConfig(config);
13327
+ res.json(loadGlobalNotificationsConfig());
13328
+ });
13329
+ return router;
13330
+ }
12793
13331
  function createProjectConfigRoutes() {
12794
13332
  const router = Router3({ mergeParams: true });
12795
13333
  router.get("/config", (req, res) => {
@@ -12823,8 +13361,8 @@ function createProjectConfigRoutes() {
12823
13361
 
12824
13362
  // ../server/dist/routes/doctor.routes.js
12825
13363
  init_dist();
12826
- import * as fs29 from "fs";
12827
- import * as path30 from "path";
13364
+ import * as fs31 from "fs";
13365
+ import * as path32 from "path";
12828
13366
  import { execSync as execSync6 } from "child_process";
12829
13367
  import { Router as Router4 } from "express";
12830
13368
  function runDoctorChecks(projectDir, config) {
@@ -12857,7 +13395,7 @@ function runDoctorChecks(projectDir, config) {
12857
13395
  });
12858
13396
  }
12859
13397
  try {
12860
- const projectName = path30.basename(projectDir);
13398
+ const projectName = path32.basename(projectDir);
12861
13399
  const marker = generateMarker(projectName);
12862
13400
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
12863
13401
  if (crontabEntries.length > 0) {
@@ -12880,8 +13418,8 @@ function runDoctorChecks(projectDir, config) {
12880
13418
  detail: "Failed to check crontab"
12881
13419
  });
12882
13420
  }
12883
- const configPath = path30.join(projectDir, CONFIG_FILE_NAME);
12884
- if (fs29.existsSync(configPath)) {
13421
+ const configPath = path32.join(projectDir, CONFIG_FILE_NAME);
13422
+ if (fs31.existsSync(configPath)) {
12885
13423
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
12886
13424
  } else {
12887
13425
  checks.push({
@@ -12890,9 +13428,9 @@ function runDoctorChecks(projectDir, config) {
12890
13428
  detail: "Config file not found (using defaults)"
12891
13429
  });
12892
13430
  }
12893
- const prdDir = path30.join(projectDir, config.prdDir);
12894
- if (fs29.existsSync(prdDir)) {
12895
- const prds = fs29.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
13431
+ const prdDir = path32.join(projectDir, config.prdDir);
13432
+ if (fs31.existsSync(prdDir)) {
13433
+ const prds = fs31.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
12896
13434
  checks.push({
12897
13435
  name: "prdDir",
12898
13436
  status: "pass",
@@ -12935,7 +13473,7 @@ function createProjectDoctorRoutes() {
12935
13473
 
12936
13474
  // ../server/dist/routes/log.routes.js
12937
13475
  init_dist();
12938
- import * as path31 from "path";
13476
+ import * as path33 from "path";
12939
13477
  import { Router as Router5 } from "express";
12940
13478
  function createLogRoutes(deps) {
12941
13479
  const { projectDir } = deps;
@@ -12943,7 +13481,7 @@ function createLogRoutes(deps) {
12943
13481
  router.get("/:name", (req, res) => {
12944
13482
  try {
12945
13483
  const { name } = req.params;
12946
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13484
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12947
13485
  if (!validNames.includes(name)) {
12948
13486
  res.status(400).json({
12949
13487
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12954,7 +13492,7 @@ function createLogRoutes(deps) {
12954
13492
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12955
13493
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12956
13494
  const fileName = LOG_FILE_NAMES[name] || name;
12957
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13495
+ const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
12958
13496
  const logLines = getLastLogLines(logPath, linesToRead);
12959
13497
  res.json({ name, lines: logLines });
12960
13498
  } catch (error2) {
@@ -12969,7 +13507,7 @@ function createProjectLogRoutes() {
12969
13507
  try {
12970
13508
  const projectDir = req.projectDir;
12971
13509
  const { name } = req.params;
12972
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13510
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12973
13511
  if (!validNames.includes(name)) {
12974
13512
  res.status(400).json({
12975
13513
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12980,7 +13518,7 @@ function createProjectLogRoutes() {
12980
13518
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12981
13519
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12982
13520
  const fileName = LOG_FILE_NAMES[name] || name;
12983
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13521
+ const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
12984
13522
  const logLines = getLastLogLines(logPath, linesToRead);
12985
13523
  res.json({ name, lines: logLines });
12986
13524
  } catch (error2) {
@@ -13015,7 +13553,7 @@ function createProjectPrdRoutes() {
13015
13553
 
13016
13554
  // ../server/dist/routes/roadmap.routes.js
13017
13555
  init_dist();
13018
- import * as path32 from "path";
13556
+ import * as path34 from "path";
13019
13557
  import { Router as Router7 } from "express";
13020
13558
  function createRoadmapRouteHandlers(ctx) {
13021
13559
  const router = Router7({ mergeParams: true });
@@ -13025,7 +13563,7 @@ function createRoadmapRouteHandlers(ctx) {
13025
13563
  const config = ctx.getConfig(req);
13026
13564
  const projectDir = ctx.getProjectDir(req);
13027
13565
  const status = getRoadmapStatus(projectDir, config);
13028
- const prdDir = path32.join(projectDir, config.prdDir);
13566
+ const prdDir = path34.join(projectDir, config.prdDir);
13029
13567
  const state = loadRoadmapState(prdDir);
13030
13568
  res.json({
13031
13569
  ...status,
@@ -13149,11 +13687,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13149
13687
  const qaPlan = getSchedulingPlan(projectDir, config, "qa");
13150
13688
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
13151
13689
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
13690
+ const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
13152
13691
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13153
13692
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13154
13693
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13155
13694
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13156
13695
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13696
+ const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
13157
13697
  return {
13158
13698
  executor: {
13159
13699
  schedule: config.cronSchedule,
@@ -13195,6 +13735,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13195
13735
  manualDelayMinutes: plannerPlan.manualDelayMinutes,
13196
13736
  balancedDelayMinutes: plannerPlan.balancedDelayMinutes
13197
13737
  },
13738
+ analytics: {
13739
+ schedule: config.analytics.schedule,
13740
+ installed: analyticsInstalled,
13741
+ nextRun: analyticsInstalled ? addDelayToIsoString(computeNextRun(config.analytics.schedule), analyticsPlan.totalDelayMinutes) : null,
13742
+ delayMinutes: analyticsPlan.totalDelayMinutes,
13743
+ manualDelayMinutes: analyticsPlan.manualDelayMinutes,
13744
+ balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
13745
+ },
13198
13746
  paused: !installed,
13199
13747
  schedulingPriority: config.schedulingPriority,
13200
13748
  entries
@@ -13347,26 +13895,26 @@ function createQueueRoutes(deps) {
13347
13895
 
13348
13896
  // ../server/dist/index.js
13349
13897
  var __filename2 = fileURLToPath3(import.meta.url);
13350
- var __dirname3 = dirname7(__filename2);
13898
+ var __dirname3 = dirname8(__filename2);
13351
13899
  function resolveWebDistPath() {
13352
- const bundled = path33.join(__dirname3, "web");
13353
- if (fs30.existsSync(path33.join(bundled, "index.html")))
13900
+ const bundled = path35.join(__dirname3, "web");
13901
+ if (fs32.existsSync(path35.join(bundled, "index.html")))
13354
13902
  return bundled;
13355
13903
  let d = __dirname3;
13356
13904
  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")))
13905
+ if (fs32.existsSync(path35.join(d, "turbo.json"))) {
13906
+ const dev = path35.join(d, "web/dist");
13907
+ if (fs32.existsSync(path35.join(dev, "index.html")))
13360
13908
  return dev;
13361
13909
  break;
13362
13910
  }
13363
- d = dirname7(d);
13911
+ d = dirname8(d);
13364
13912
  }
13365
13913
  return bundled;
13366
13914
  }
13367
13915
  function setupStaticFiles(app) {
13368
13916
  const webDistPath = resolveWebDistPath();
13369
- if (fs30.existsSync(webDistPath)) {
13917
+ if (fs32.existsSync(webDistPath)) {
13370
13918
  app.use(express.static(webDistPath));
13371
13919
  }
13372
13920
  app.use((req, res, next) => {
@@ -13374,8 +13922,8 @@ function setupStaticFiles(app) {
13374
13922
  next();
13375
13923
  return;
13376
13924
  }
13377
- const indexPath = path33.resolve(webDistPath, "index.html");
13378
- if (fs30.existsSync(indexPath)) {
13925
+ const indexPath = path35.resolve(webDistPath, "index.html");
13926
+ if (fs32.existsSync(indexPath)) {
13379
13927
  res.sendFile(indexPath, (err) => {
13380
13928
  if (err)
13381
13929
  next();
@@ -13413,6 +13961,7 @@ function createApp(projectDir) {
13413
13961
  app.use("/api/logs", createLogRoutes({ projectDir }));
13414
13962
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
13415
13963
  app.use("/api/queue", createQueueRoutes({ getConfig: () => config }));
13964
+ app.use("/api/global-notifications", createGlobalNotificationsRoutes());
13416
13965
  app.get("/api/prs", async (_req, res) => {
13417
13966
  try {
13418
13967
  res.json(await collectPrInfo(projectDir, config.branchPatterns));
@@ -13485,13 +14034,14 @@ function createGlobalApp() {
13485
14034
  }
13486
14035
  });
13487
14036
  app.use("/api/queue", createGlobalQueueRoutes());
14037
+ app.use("/api/global-notifications", createGlobalNotificationsRoutes());
13488
14038
  app.use("/api/projects/:projectId", resolveProject, createProjectRouter());
13489
14039
  setupStaticFiles(app);
13490
14040
  app.use(errorHandler);
13491
14041
  return app;
13492
14042
  }
13493
14043
  function bootContainer() {
13494
- initContainer(path33.dirname(getDbPath()));
14044
+ initContainer(path35.dirname(getDbPath()));
13495
14045
  }
13496
14046
  function startServer(projectDir, port) {
13497
14047
  bootContainer();
@@ -13544,8 +14094,8 @@ function isProcessRunning2(pid) {
13544
14094
  }
13545
14095
  function readPid(lockPath) {
13546
14096
  try {
13547
- if (!fs31.existsSync(lockPath)) return null;
13548
- const raw = fs31.readFileSync(lockPath, "utf-8").trim();
14097
+ if (!fs33.existsSync(lockPath)) return null;
14098
+ const raw = fs33.readFileSync(lockPath, "utf-8").trim();
13549
14099
  const pid = parseInt(raw, 10);
13550
14100
  return Number.isFinite(pid) ? pid : null;
13551
14101
  } catch {
@@ -13557,10 +14107,10 @@ function acquireServeLock(mode, port) {
13557
14107
  let stalePidCleaned;
13558
14108
  for (let attempt = 0; attempt < 2; attempt++) {
13559
14109
  try {
13560
- const fd = fs31.openSync(lockPath, "wx");
13561
- fs31.writeFileSync(fd, `${process.pid}
14110
+ const fd = fs33.openSync(lockPath, "wx");
14111
+ fs33.writeFileSync(fd, `${process.pid}
13562
14112
  `);
13563
- fs31.closeSync(fd);
14113
+ fs33.closeSync(fd);
13564
14114
  return { acquired: true, lockPath, stalePidCleaned };
13565
14115
  } catch (error2) {
13566
14116
  const err = error2;
@@ -13581,7 +14131,7 @@ function acquireServeLock(mode, port) {
13581
14131
  };
13582
14132
  }
13583
14133
  try {
13584
- fs31.unlinkSync(lockPath);
14134
+ fs33.unlinkSync(lockPath);
13585
14135
  if (existingPid) {
13586
14136
  stalePidCleaned = existingPid;
13587
14137
  }
@@ -13604,10 +14154,10 @@ function acquireServeLock(mode, port) {
13604
14154
  }
13605
14155
  function releaseServeLock(lockPath) {
13606
14156
  try {
13607
- if (!fs31.existsSync(lockPath)) return;
14157
+ if (!fs33.existsSync(lockPath)) return;
13608
14158
  const lockPid = readPid(lockPath);
13609
14159
  if (lockPid !== null && lockPid !== process.pid) return;
13610
- fs31.unlinkSync(lockPath);
14160
+ fs33.unlinkSync(lockPath);
13611
14161
  } catch {
13612
14162
  }
13613
14163
  }
@@ -13703,14 +14253,14 @@ function historyCommand(program2) {
13703
14253
  // src/commands/update.ts
13704
14254
  init_dist();
13705
14255
  import { spawnSync } from "child_process";
13706
- import * as fs32 from "fs";
13707
- import * as path34 from "path";
14256
+ import * as fs34 from "fs";
14257
+ import * as path36 from "path";
13708
14258
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
13709
14259
  function parseProjectDirs(projects, cwd) {
13710
14260
  if (!projects || projects.trim().length === 0) {
13711
14261
  return [cwd];
13712
14262
  }
13713
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path34.resolve(cwd, entry));
14263
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path36.resolve(cwd, entry));
13714
14264
  return Array.from(new Set(dirs));
13715
14265
  }
13716
14266
  function shouldInstallGlobal(options) {
@@ -13752,7 +14302,7 @@ function updateCommand(program2) {
13752
14302
  }
13753
14303
  const nightWatchBin = resolveNightWatchBin();
13754
14304
  for (const projectDir of projectDirs) {
13755
- if (!fs32.existsSync(projectDir) || !fs32.statSync(projectDir).isDirectory()) {
14305
+ if (!fs34.existsSync(projectDir) || !fs34.statSync(projectDir).isDirectory()) {
13756
14306
  warn(`Skipping invalid project directory: ${projectDir}`);
13757
14307
  continue;
13758
14308
  }
@@ -13796,8 +14346,8 @@ function prdStateCommand(program2) {
13796
14346
 
13797
14347
  // src/commands/retry.ts
13798
14348
  init_dist();
13799
- import * as fs33 from "fs";
13800
- import * as path35 from "path";
14349
+ import * as fs35 from "fs";
14350
+ import * as path37 from "path";
13801
14351
  function normalizePrdName(name) {
13802
14352
  if (!name.endsWith(".md")) {
13803
14353
  return `${name}.md`;
@@ -13805,26 +14355,26 @@ function normalizePrdName(name) {
13805
14355
  return name;
13806
14356
  }
13807
14357
  function getDonePrds(doneDir) {
13808
- if (!fs33.existsSync(doneDir)) {
14358
+ if (!fs35.existsSync(doneDir)) {
13809
14359
  return [];
13810
14360
  }
13811
- return fs33.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14361
+ return fs35.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
13812
14362
  }
13813
14363
  function retryCommand(program2) {
13814
14364
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
13815
14365
  const projectDir = process.cwd();
13816
14366
  const config = loadConfig(projectDir);
13817
- const prdDir = path35.join(projectDir, config.prdDir);
13818
- const doneDir = path35.join(prdDir, "done");
14367
+ const prdDir = path37.join(projectDir, config.prdDir);
14368
+ const doneDir = path37.join(prdDir, "done");
13819
14369
  const normalizedPrdName = normalizePrdName(prdName);
13820
- const pendingPath = path35.join(prdDir, normalizedPrdName);
13821
- if (fs33.existsSync(pendingPath)) {
14370
+ const pendingPath = path37.join(prdDir, normalizedPrdName);
14371
+ if (fs35.existsSync(pendingPath)) {
13822
14372
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
13823
14373
  return;
13824
14374
  }
13825
- const donePath = path35.join(doneDir, normalizedPrdName);
13826
- if (fs33.existsSync(donePath)) {
13827
- fs33.renameSync(donePath, pendingPath);
14375
+ const donePath = path37.join(doneDir, normalizedPrdName);
14376
+ if (fs35.existsSync(donePath)) {
14377
+ fs35.renameSync(donePath, pendingPath);
13828
14378
  success(`Moved "${normalizedPrdName}" back to pending.`);
13829
14379
  dim(`From: ${donePath}`);
13830
14380
  dim(`To: ${pendingPath}`);
@@ -14076,7 +14626,7 @@ function prdsCommand(program2) {
14076
14626
 
14077
14627
  // src/commands/cancel.ts
14078
14628
  init_dist();
14079
- import * as fs34 from "fs";
14629
+ import * as fs36 from "fs";
14080
14630
  import * as readline3 from "readline";
14081
14631
  function getLockFilePaths2(projectDir) {
14082
14632
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -14123,7 +14673,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14123
14673
  const pid = lockStatus.pid;
14124
14674
  if (!lockStatus.running) {
14125
14675
  try {
14126
- fs34.unlinkSync(lockPath);
14676
+ fs36.unlinkSync(lockPath);
14127
14677
  return {
14128
14678
  success: true,
14129
14679
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -14161,7 +14711,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14161
14711
  await sleep2(3e3);
14162
14712
  if (!isProcessRunning3(pid)) {
14163
14713
  try {
14164
- fs34.unlinkSync(lockPath);
14714
+ fs36.unlinkSync(lockPath);
14165
14715
  } catch {
14166
14716
  }
14167
14717
  return {
@@ -14196,7 +14746,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14196
14746
  await sleep2(500);
14197
14747
  if (!isProcessRunning3(pid)) {
14198
14748
  try {
14199
- fs34.unlinkSync(lockPath);
14749
+ fs36.unlinkSync(lockPath);
14200
14750
  } catch {
14201
14751
  }
14202
14752
  return {
@@ -14257,31 +14807,31 @@ function cancelCommand(program2) {
14257
14807
 
14258
14808
  // src/commands/slice.ts
14259
14809
  init_dist();
14260
- import * as fs35 from "fs";
14261
- import * as path36 from "path";
14810
+ import * as fs37 from "fs";
14811
+ import * as path38 from "path";
14262
14812
  function plannerLockPath2(projectDir) {
14263
14813
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
14264
14814
  }
14265
14815
  function acquirePlannerLock(projectDir) {
14266
14816
  const lockFile = plannerLockPath2(projectDir);
14267
- if (fs35.existsSync(lockFile)) {
14268
- const pidRaw = fs35.readFileSync(lockFile, "utf-8").trim();
14817
+ if (fs37.existsSync(lockFile)) {
14818
+ const pidRaw = fs37.readFileSync(lockFile, "utf-8").trim();
14269
14819
  const pid = parseInt(pidRaw, 10);
14270
14820
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
14271
14821
  return { acquired: false, lockFile, pid };
14272
14822
  }
14273
14823
  try {
14274
- fs35.unlinkSync(lockFile);
14824
+ fs37.unlinkSync(lockFile);
14275
14825
  } catch {
14276
14826
  }
14277
14827
  }
14278
- fs35.writeFileSync(lockFile, String(process.pid));
14828
+ fs37.writeFileSync(lockFile, String(process.pid));
14279
14829
  return { acquired: true, lockFile };
14280
14830
  }
14281
14831
  function releasePlannerLock(lockFile) {
14282
14832
  try {
14283
- if (fs35.existsSync(lockFile)) {
14284
- fs35.unlinkSync(lockFile);
14833
+ if (fs37.existsSync(lockFile)) {
14834
+ fs37.unlinkSync(lockFile);
14285
14835
  }
14286
14836
  } catch {
14287
14837
  }
@@ -14290,12 +14840,12 @@ function resolvePlannerIssueColumn(config) {
14290
14840
  return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
14291
14841
  }
14292
14842
  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 ?? "");
14843
+ const relativePrdPath = path38.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
14844
+ const absolutePrdPath = path38.join(projectDir, config.prdDir, result.file ?? "");
14295
14845
  const sourceItem = result.item;
14296
14846
  let prdContent;
14297
14847
  try {
14298
- prdContent = fs35.readFileSync(absolutePrdPath, "utf-8");
14848
+ prdContent = fs37.readFileSync(absolutePrdPath, "utf-8");
14299
14849
  } catch {
14300
14850
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
14301
14851
  }
@@ -14471,7 +15021,7 @@ function sliceCommand(program2) {
14471
15021
  if (!options.dryRun) {
14472
15022
  await sendNotifications(config, {
14473
15023
  event: "run_started",
14474
- projectName: path36.basename(projectDir),
15024
+ projectName: path38.basename(projectDir),
14475
15025
  exitCode: 0,
14476
15026
  provider: config.provider
14477
15027
  });
@@ -14506,7 +15056,7 @@ function sliceCommand(program2) {
14506
15056
  if (!options.dryRun && result.sliced) {
14507
15057
  await sendNotifications(config, {
14508
15058
  event: "run_succeeded",
14509
- projectName: path36.basename(projectDir),
15059
+ projectName: path38.basename(projectDir),
14510
15060
  exitCode,
14511
15061
  provider: config.provider,
14512
15062
  prTitle: result.item?.title
@@ -14514,7 +15064,7 @@ function sliceCommand(program2) {
14514
15064
  } else if (!options.dryRun && !nothingPending) {
14515
15065
  await sendNotifications(config, {
14516
15066
  event: "run_failed",
14517
- projectName: path36.basename(projectDir),
15067
+ projectName: path38.basename(projectDir),
14518
15068
  exitCode,
14519
15069
  provider: config.provider
14520
15070
  });
@@ -14530,21 +15080,21 @@ function sliceCommand(program2) {
14530
15080
 
14531
15081
  // src/commands/state.ts
14532
15082
  init_dist();
14533
- import * as os7 from "os";
14534
- import * as path37 from "path";
15083
+ import * as os9 from "os";
15084
+ import * as path39 from "path";
14535
15085
  import chalk5 from "chalk";
14536
15086
  import { Command } from "commander";
14537
15087
  function createStateCommand() {
14538
15088
  const state = new Command("state");
14539
15089
  state.description("Manage Night Watch state");
14540
15090
  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);
15091
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path39.join(os9.homedir(), GLOBAL_CONFIG_DIR);
14542
15092
  if (opts.dryRun) {
14543
15093
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
14544
15094
  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")}`);
15095
+ console.log(` ${path39.join(nightWatchHome, "projects.json")}`);
15096
+ console.log(` ${path39.join(nightWatchHome, "history.json")}`);
15097
+ console.log(` ${path39.join(nightWatchHome, "prd-states.json")}`);
14548
15098
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
14549
15099
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
14550
15100
  return;
@@ -14582,8 +15132,8 @@ function createStateCommand() {
14582
15132
  init_dist();
14583
15133
  init_dist();
14584
15134
  import { execFileSync as execFileSync6 } from "child_process";
14585
- import * as fs36 from "fs";
14586
- import * as path38 from "path";
15135
+ import * as fs38 from "fs";
15136
+ import * as path40 from "path";
14587
15137
  import * as readline4 from "readline";
14588
15138
  import chalk6 from "chalk";
14589
15139
  async function run(fn) {
@@ -14605,7 +15155,7 @@ function getProvider(config, cwd) {
14605
15155
  return createBoardProvider(bp, cwd);
14606
15156
  }
14607
15157
  function defaultBoardTitle(cwd) {
14608
- return `${path38.basename(cwd)} Night Watch`;
15158
+ return `${path40.basename(cwd)} Night Watch`;
14609
15159
  }
14610
15160
  async function ensureBoardConfigured(config, cwd, provider, options) {
14611
15161
  if (config.boardProvider?.projectNumber) {
@@ -14804,11 +15354,11 @@ function boardCommand(program2) {
14804
15354
  let body = options.body ?? "";
14805
15355
  if (options.bodyFile) {
14806
15356
  const filePath = options.bodyFile;
14807
- if (!fs36.existsSync(filePath)) {
15357
+ if (!fs38.existsSync(filePath)) {
14808
15358
  console.error(`File not found: ${filePath}`);
14809
15359
  process.exit(1);
14810
15360
  }
14811
- body = fs36.readFileSync(filePath, "utf-8");
15361
+ body = fs38.readFileSync(filePath, "utf-8");
14812
15362
  }
14813
15363
  const labels = [];
14814
15364
  if (options.label) {
@@ -14836,6 +15386,25 @@ function boardCommand(program2) {
14836
15386
  }
14837
15387
  })
14838
15388
  );
15389
+ board.command("add-issue").description("Add an existing GitHub issue to the board").argument("<number>", "Issue number").option("--column <name>", "Target column (default: Ready)", "Ready").action(
15390
+ async (number, options) => run(async () => {
15391
+ const cwd = process.cwd();
15392
+ const config = loadConfig(cwd);
15393
+ const provider = getProvider(config, cwd);
15394
+ await ensureBoardConfigured(config, cwd, provider);
15395
+ if (!BOARD_COLUMNS.includes(options.column)) {
15396
+ console.error(
15397
+ `Invalid column "${options.column}". Valid columns: ${BOARD_COLUMNS.join(", ")}`
15398
+ );
15399
+ process.exit(1);
15400
+ }
15401
+ const issue = await provider.addIssue(
15402
+ parseInt(number, 10),
15403
+ options.column
15404
+ );
15405
+ success(`Added issue #${issue.number} "${issue.title}" to ${options.column}`);
15406
+ })
15407
+ );
14839
15408
  board.command("status").description("Show the current state of all issues grouped by column").option("--json", "Output raw JSON").option("--group-by <field>", "Group by: priority, category, or column (default: column)").action(
14840
15409
  async (options) => run(async () => {
14841
15410
  const cwd = process.cwd();
@@ -15030,12 +15599,12 @@ function boardCommand(program2) {
15030
15599
  const config = loadConfig(cwd);
15031
15600
  const provider = getProvider(config, cwd);
15032
15601
  await ensureBoardConfigured(config, cwd, provider);
15033
- const roadmapPath = options.roadmap ?? path38.join(cwd, "ROADMAP.md");
15034
- if (!fs36.existsSync(roadmapPath)) {
15602
+ const roadmapPath = options.roadmap ?? path40.join(cwd, "ROADMAP.md");
15603
+ if (!fs38.existsSync(roadmapPath)) {
15035
15604
  console.error(`Roadmap file not found: ${roadmapPath}`);
15036
15605
  process.exit(1);
15037
15606
  }
15038
- const roadmapContent = fs36.readFileSync(roadmapPath, "utf-8");
15607
+ const roadmapContent = fs38.readFileSync(roadmapPath, "utf-8");
15039
15608
  const items = parseRoadmap(roadmapContent);
15040
15609
  const uncheckedItems = getUncheckedItems(items);
15041
15610
  if (uncheckedItems.length === 0) {
@@ -15159,11 +15728,11 @@ function boardCommand(program2) {
15159
15728
  // src/commands/queue.ts
15160
15729
  init_dist();
15161
15730
  init_dist();
15162
- import * as path39 from "path";
15731
+ import * as path41 from "path";
15163
15732
  import { spawn as spawn6 } from "child_process";
15164
15733
  import chalk7 from "chalk";
15165
15734
  import { Command as Command2 } from "commander";
15166
- var logger2 = createLogger("queue");
15735
+ var logger4 = createLogger("queue");
15167
15736
  var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
15168
15737
  function formatTimestamp(unixTs) {
15169
15738
  if (unixTs === null) return "-";
@@ -15279,7 +15848,7 @@ function createQueueCommand() {
15279
15848
  process.exit(1);
15280
15849
  }
15281
15850
  }
15282
- const projectName = path39.basename(projectDir);
15851
+ const projectName = path41.basename(projectDir);
15283
15852
  const queueConfig = loadConfig(projectDir).queue;
15284
15853
  const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
15285
15854
  console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
@@ -15287,13 +15856,13 @@ function createQueueCommand() {
15287
15856
  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
15857
  const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
15289
15858
  if (!entry) {
15290
- logger2.info("No pending jobs to dispatch");
15859
+ logger4.info("No pending jobs to dispatch");
15291
15860
  return;
15292
15861
  }
15293
- logger2.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15862
+ logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15294
15863
  const scriptName = getScriptNameForJobType(entry.jobType);
15295
15864
  if (!scriptName) {
15296
- logger2.error(`Unknown job type: ${entry.jobType}`);
15865
+ logger4.error(`Unknown job type: ${entry.jobType}`);
15297
15866
  return;
15298
15867
  }
15299
15868
  let projectEnv;
@@ -15312,7 +15881,7 @@ function createQueueCommand() {
15312
15881
  NW_QUEUE_ENTRY_ID: String(entry.id)
15313
15882
  };
15314
15883
  const scriptPath = getScriptPath(scriptName);
15315
- logger2.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15884
+ logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15316
15885
  try {
15317
15886
  const child = spawn6("bash", [scriptPath, entry.projectPath], {
15318
15887
  detached: true,
@@ -15321,11 +15890,11 @@ function createQueueCommand() {
15321
15890
  cwd: entry.projectPath
15322
15891
  });
15323
15892
  child.unref();
15324
- logger2.info(`Spawned PID: ${child.pid}`);
15893
+ logger4.info(`Spawned PID: ${child.pid}`);
15325
15894
  markJobRunning(entry.id);
15326
15895
  } catch (error2) {
15327
15896
  updateJobStatus(entry.id, "pending");
15328
- logger2.error(
15897
+ logger4.error(
15329
15898
  `Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
15330
15899
  );
15331
15900
  process.exit(1);
@@ -15435,17 +16004,17 @@ function notifyCommand(program2) {
15435
16004
 
15436
16005
  // src/cli.ts
15437
16006
  var __filename3 = fileURLToPath4(import.meta.url);
15438
- var __dirname4 = dirname8(__filename3);
16007
+ var __dirname4 = dirname9(__filename3);
15439
16008
  function findPackageRoot(dir) {
15440
16009
  let d = dir;
15441
16010
  for (let i = 0; i < 5; i++) {
15442
- if (existsSync29(join34(d, "package.json"))) return d;
15443
- d = dirname8(d);
16011
+ if (existsSync30(join36(d, "package.json"))) return d;
16012
+ d = dirname9(d);
15444
16013
  }
15445
16014
  return dir;
15446
16015
  }
15447
16016
  var packageRoot = findPackageRoot(__dirname4);
15448
- var packageJson = JSON.parse(readFileSync18(join34(packageRoot, "package.json"), "utf-8"));
16017
+ var packageJson = JSON.parse(readFileSync19(join36(packageRoot, "package.json"), "utf-8"));
15449
16018
  var program = new Command3();
15450
16019
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
15451
16020
  initCommand(program);
@@ -15453,6 +16022,7 @@ runCommand(program);
15453
16022
  reviewCommand(program);
15454
16023
  qaCommand(program);
15455
16024
  auditCommand(program);
16025
+ analyticsCommand(program);
15456
16026
  installCommand(program);
15457
16027
  uninstallCommand(program);
15458
16028
  statusCommand(program);