@jonit-dev/night-watch-cli 1.8.14-beta.6 → 1.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/cli.js +1750 -422
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/doctor.d.ts.map +1 -1
  4. package/dist/commands/doctor.js +16 -0
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +13 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/install.d.ts +4 -0
  10. package/dist/commands/install.d.ts.map +1 -1
  11. package/dist/commands/install.js +26 -4
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/logs.d.ts.map +1 -1
  14. package/dist/commands/logs.js +14 -4
  15. package/dist/commands/logs.js.map +1 -1
  16. package/dist/commands/merge.d.ts.map +1 -1
  17. package/dist/commands/merge.js +20 -0
  18. package/dist/commands/merge.js.map +1 -1
  19. package/dist/commands/queue.d.ts.map +1 -1
  20. package/dist/commands/queue.js +11 -7
  21. package/dist/commands/queue.js.map +1 -1
  22. package/dist/commands/review.d.ts.map +1 -1
  23. package/dist/commands/review.js +26 -0
  24. package/dist/commands/review.js.map +1 -1
  25. package/dist/commands/run.d.ts.map +1 -1
  26. package/dist/commands/run.js +24 -0
  27. package/dist/commands/run.js.map +1 -1
  28. package/dist/commands/serve.d.ts +1 -0
  29. package/dist/commands/serve.d.ts.map +1 -1
  30. package/dist/commands/serve.js +10 -4
  31. package/dist/commands/serve.js.map +1 -1
  32. package/dist/commands/shared/telemetry.d.ts +27 -0
  33. package/dist/commands/shared/telemetry.d.ts.map +1 -0
  34. package/dist/commands/shared/telemetry.js +73 -0
  35. package/dist/commands/shared/telemetry.js.map +1 -0
  36. package/dist/commands/status.d.ts.map +1 -1
  37. package/dist/commands/status.js +24 -0
  38. package/dist/commands/status.js.map +1 -1
  39. package/dist/commands/telemetry.d.ts +3 -0
  40. package/dist/commands/telemetry.d.ts.map +1 -0
  41. package/dist/commands/telemetry.js +52 -0
  42. package/dist/commands/telemetry.js.map +1 -0
  43. package/dist/commands/ux.d.ts +14 -0
  44. package/dist/commands/ux.d.ts.map +1 -0
  45. package/dist/commands/ux.js +169 -0
  46. package/dist/commands/ux.js.map +1 -0
  47. package/dist/scripts/night-watch-cron.sh +36 -12
  48. package/dist/telemetry-bootstrap.d.ts +2 -0
  49. package/dist/telemetry-bootstrap.d.ts.map +1 -0
  50. package/dist/telemetry-bootstrap.js +32 -0
  51. package/dist/telemetry-bootstrap.js.map +1 -0
  52. package/dist/templates/night-watch.config.json +14 -0
  53. package/dist/web/assets/index-BheLL2O2.css +1 -0
  54. package/dist/web/assets/index-BsTuwxzi.js +447 -0
  55. package/dist/web/index.html +2 -2
  56. package/package.json +2 -2
  57. package/dist/web/assets/index-6Yf-Q6Di.js +0 -442
  58. package/dist/web/assets/index-B6E6kOoR.js +0 -406
  59. package/dist/web/assets/index-C-xpWpS8.css +0 -1
  60. package/dist/web/assets/index-C3BV1Hoa.js +0 -406
  61. package/dist/web/assets/index-CEYe-290.js +0 -412
  62. package/dist/web/assets/index-CL3Q-KB4.css +0 -1
  63. package/dist/web/assets/index-DIMUXIP8.css +0 -1
  64. package/dist/web/assets/index-DatF4suf.css +0 -1
  65. package/dist/web/assets/index-DpvzoXEv.js +0 -442
  66. package/dist/web/assets/index-Ds8OqaCa.css +0 -1
  67. package/dist/web/assets/index-DyME41HV.css +0 -1
  68. package/dist/web/assets/index-FDOCfjkP.js +0 -442
  69. package/dist/web/assets/index-NR27JE3b.js +0 -406
  70. package/dist/web/assets/index-Q3IYCcdZ.js +0 -447
package/dist/cli.js CHANGED
@@ -379,6 +379,42 @@ var init_job_registry = __esm({
379
379
  targetColumn: "Draft"
380
380
  }
381
381
  },
382
+ {
383
+ id: "ux",
384
+ name: "UX",
385
+ description: "Inspects user flows with browser automation and drafts prioritized UX reports",
386
+ cliCommand: "ux",
387
+ logName: "ux",
388
+ lockSuffix: "-ux.lock",
389
+ queuePriority: 10,
390
+ envPrefix: "NW_UX",
391
+ extraFields: [
392
+ {
393
+ name: "targetColumn",
394
+ type: "enum",
395
+ enumValues: [...BOARD_COLUMNS],
396
+ defaultValue: "Draft"
397
+ },
398
+ { name: "baseUrl", type: "string", defaultValue: "" },
399
+ { name: "startUrl", type: "string", defaultValue: "" },
400
+ { name: "flows", type: "string[]", defaultValue: [] },
401
+ { name: "autoInstallPlaywright", type: "boolean", defaultValue: true },
402
+ { name: "maxIssues", type: "number", defaultValue: 10 },
403
+ { name: "reportPrompt", type: "string", defaultValue: "" }
404
+ ],
405
+ defaultConfig: {
406
+ enabled: false,
407
+ schedule: "0 7 * * 1",
408
+ maxRuntime: 0,
409
+ targetColumn: "Draft",
410
+ baseUrl: "",
411
+ startUrl: "",
412
+ flows: [],
413
+ autoInstallPlaywright: true,
414
+ maxIssues: 10,
415
+ reportPrompt: ""
416
+ }
417
+ },
382
418
  {
383
419
  id: "analytics",
384
420
  name: "Analytics",
@@ -471,7 +507,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
471
507
  return `claude-proxy:${baseUrl}`;
472
508
  }
473
509
  }
474
- var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_SUMMARY_WINDOW_HOURS, 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_FEEDBACK, 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_VALIDATED_LABEL, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT_CREATE_ISSUES, DEFAULT_AUDIT_TARGET_COLUMN, 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, DEFAULT_MANAGER_ENABLED, DEFAULT_MANAGER_SCHEDULE, DEFAULT_MANAGER_MAX_RUNTIME, DEFAULT_MANAGER_AUTHORITY, DEFAULT_MANAGER_OUTPUT_MODE, DEFAULT_MANAGER_TARGET_COLUMN, DEFAULT_MANAGER_MEMORY_PATH, DEFAULT_MANAGER_DOCS_DIR, DEFAULT_MANAGER_WEEKLY_SUMMARY_ENABLED, DEFAULT_MANAGER_WEEKLY_SUMMARY_DAY, DEFAULT_MANAGER, DEFAULT_PR_RESOLVER_ENABLED, DEFAULT_PR_RESOLVER_SCHEDULE, DEFAULT_PR_RESOLVER_MAX_RUNTIME, DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN, DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT, DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION, DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION, DEFAULT_PR_RESOLVER_READY_LABEL, DEFAULT_PR_RESOLVER, DEFAULT_MERGER_ENABLED, DEFAULT_MERGER_SCHEDULE, DEFAULT_MERGER_MAX_RUNTIME, DEFAULT_MERGER_MERGE_METHOD, DEFAULT_MERGER_MIN_REVIEW_SCORE, DEFAULT_MERGER_REBASE_BEFORE_MERGE, DEFAULT_MERGER_MAX_PRS_PER_RUN, DEFAULT_MERGER_CI_POLICY, DEFAULT_MERGER_LOCAL_CHECK_COMMAND, DEFAULT_MERGER, MERGER_LOG_NAME, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, PR_RESOLVER_LOG_NAME, MANAGER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS, DEFAULT_WEBHOOK_TRIGGERS, 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;
510
+ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_SUMMARY_WINDOW_HOURS, 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_FEEDBACK, 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_MODEL_ATTRIBUTION, DEFAULT_NEW_PR_LABEL, 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_VALIDATED_LABEL, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT_CREATE_ISSUES, DEFAULT_AUDIT_TARGET_COLUMN, DEFAULT_AUDIT, DEFAULT_UX_ENABLED, DEFAULT_UX_SCHEDULE, DEFAULT_UX_MAX_RUNTIME, DEFAULT_UX_TARGET_COLUMN, DEFAULT_UX_BASE_URL, DEFAULT_UX_START_URL, DEFAULT_UX_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_UX_MAX_ISSUES, DEFAULT_UX_REPORT_PROMPT, DEFAULT_UX, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, DEFAULT_MANAGER_ENABLED, DEFAULT_MANAGER_SCHEDULE, DEFAULT_MANAGER_MAX_RUNTIME, DEFAULT_MANAGER_AUTHORITY, DEFAULT_MANAGER_OUTPUT_MODE, DEFAULT_MANAGER_TARGET_COLUMN, DEFAULT_MANAGER_MEMORY_PATH, DEFAULT_MANAGER_DOCS_DIR, DEFAULT_MANAGER_WEEKLY_SUMMARY_ENABLED, DEFAULT_MANAGER_WEEKLY_SUMMARY_DAY, DEFAULT_MANAGER, DEFAULT_PR_RESOLVER_ENABLED, DEFAULT_PR_RESOLVER_SCHEDULE, DEFAULT_PR_RESOLVER_MAX_RUNTIME, DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN, DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT, DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION, DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION, DEFAULT_PR_RESOLVER_READY_LABEL, DEFAULT_PR_RESOLVER, DEFAULT_MERGER_ENABLED, DEFAULT_MERGER_SCHEDULE, DEFAULT_MERGER_MAX_RUNTIME, DEFAULT_MERGER_MERGE_METHOD, DEFAULT_MERGER_MIN_REVIEW_SCORE, DEFAULT_MERGER_REBASE_BEFORE_MERGE, DEFAULT_MERGER_MAX_PRS_PER_RUN, DEFAULT_MERGER_CI_POLICY, DEFAULT_MERGER_LOCAL_CHECK_COMMAND, DEFAULT_MERGER, MERGER_LOG_NAME, AUDIT_LOG_NAME, UX_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, PR_RESOLVER_LOG_NAME, MANAGER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS, DEFAULT_WEBHOOK_TRIGGERS, 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, TELEMETRY_FILE_NAME, DEFAULT_AMPLITUDE_API_KEY, DEFAULT_AMPLITUDE_INGEST_ENDPOINT, 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;
475
511
  var init_constants = __esm({
476
512
  "../core/dist/constants.js"() {
477
513
  "use strict";
@@ -503,6 +539,8 @@ var init_constants = __esm({
503
539
  DEFAULT_EXECUTOR_ENABLED = true;
504
540
  DEFAULT_REVIEWER_ENABLED = true;
505
541
  DEFAULT_PROVIDER_ENV = {};
542
+ DEFAULT_MODEL_ATTRIBUTION = false;
543
+ DEFAULT_NEW_PR_LABEL = "draft";
506
544
  DEFAULT_FALLBACK_ON_RATE_LIMIT = true;
507
545
  DEFAULT_CLAUDE_MODEL = void 0;
508
546
  DEFAULT_PRIMARY_FALLBACK_MODEL = void 0;
@@ -564,6 +602,27 @@ var init_constants = __esm({
564
602
  createIssues: DEFAULT_AUDIT_CREATE_ISSUES,
565
603
  targetColumn: DEFAULT_AUDIT_TARGET_COLUMN
566
604
  };
605
+ DEFAULT_UX_ENABLED = false;
606
+ DEFAULT_UX_SCHEDULE = "0 7 * * 1";
607
+ DEFAULT_UX_MAX_RUNTIME = 0;
608
+ DEFAULT_UX_TARGET_COLUMN = "Draft";
609
+ DEFAULT_UX_BASE_URL = "";
610
+ DEFAULT_UX_START_URL = "";
611
+ DEFAULT_UX_AUTO_INSTALL_PLAYWRIGHT = true;
612
+ DEFAULT_UX_MAX_ISSUES = 10;
613
+ DEFAULT_UX_REPORT_PROMPT = "";
614
+ DEFAULT_UX = {
615
+ enabled: DEFAULT_UX_ENABLED,
616
+ schedule: DEFAULT_UX_SCHEDULE,
617
+ maxRuntime: DEFAULT_UX_MAX_RUNTIME,
618
+ targetColumn: DEFAULT_UX_TARGET_COLUMN,
619
+ baseUrl: DEFAULT_UX_BASE_URL,
620
+ startUrl: DEFAULT_UX_START_URL,
621
+ flows: [],
622
+ autoInstallPlaywright: DEFAULT_UX_AUTO_INSTALL_PLAYWRIGHT,
623
+ maxIssues: DEFAULT_UX_MAX_ISSUES,
624
+ reportPrompt: DEFAULT_UX_REPORT_PROMPT
625
+ };
567
626
  DEFAULT_ANALYTICS_ENABLED = false;
568
627
  DEFAULT_ANALYTICS_SCHEDULE = "0 6 * * 1";
569
628
  DEFAULT_ANALYTICS_MAX_RUNTIME = 0;
@@ -646,6 +705,7 @@ If no issues are warranted, output an empty array: []`;
646
705
  };
647
706
  MERGER_LOG_NAME = "merger";
648
707
  AUDIT_LOG_NAME = "audit";
708
+ UX_LOG_NAME = "ux";
649
709
  PLANNER_LOG_NAME = "slicer";
650
710
  ANALYTICS_LOG_NAME = "analytics";
651
711
  PR_RESOLVER_LOG_NAME = "pr-resolver";
@@ -747,6 +807,9 @@ If no issues are warranted, output an empty array: []`;
747
807
  PRD_STATES_FILE_NAME = "prd-states.json";
748
808
  STATE_DB_FILE_NAME = "state.db";
749
809
  GLOBAL_NOTIFICATIONS_FILE_NAME = "global-notifications.json";
810
+ TELEMETRY_FILE_NAME = "telemetry.json";
811
+ DEFAULT_AMPLITUDE_API_KEY = "5289e9a61e10e059d25f8eb846bceaa8";
812
+ DEFAULT_AMPLITUDE_INGEST_ENDPOINT = "https://api2.amplitude.com/2/httpapi";
750
813
  MAX_HISTORY_RECORDS_PER_PRD = 10;
751
814
  DEFAULT_QUEUE_ENABLED = true;
752
815
  DEFAULT_QUEUE_MODE = "auto";
@@ -826,6 +889,11 @@ function normalizeConfig(rawConfig) {
826
889
  if (providerLabelVal) {
827
890
  normalized.providerLabel = providerLabelVal;
828
891
  }
892
+ normalized.modelAttribution = readBoolean(rawConfig.modelAttribution);
893
+ const newPrLabelVal = readString2(rawConfig.newPrLabel);
894
+ if (newPrLabelVal !== void 0) {
895
+ normalized.newPrLabel = newPrLabelVal.trim();
896
+ }
829
897
  const rawProviderEnv = readObject2(rawConfig.providerEnv);
830
898
  if (rawProviderEnv) {
831
899
  const env = {};
@@ -974,7 +1042,7 @@ function normalizeConfig(rawConfig) {
974
1042
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
975
1043
  normalized.autoMergeMethod = mergeMethod;
976
1044
  }
977
- for (const jobId of ["qa", "audit", "analytics", "merger", "manager"]) {
1045
+ for (const jobId of ["qa", "audit", "ux", "analytics", "merger", "manager"]) {
978
1046
  const jobDef = getJobDef(jobId);
979
1047
  if (!jobDef)
980
1048
  continue;
@@ -1252,6 +1320,14 @@ function buildEnvOverrideConfig(fileConfig) {
1252
1320
  if (v !== null)
1253
1321
  env.executorEnabled = v;
1254
1322
  }
1323
+ if (process.env.NW_MODEL_ATTRIBUTION_ENABLED !== void 0) {
1324
+ const v = parseBoolean(process.env.NW_MODEL_ATTRIBUTION_ENABLED);
1325
+ if (v !== null)
1326
+ env.modelAttribution = v;
1327
+ }
1328
+ if (process.env.NW_NEW_PR_LABEL !== void 0) {
1329
+ env.newPrLabel = process.env.NW_NEW_PR_LABEL.trim();
1330
+ }
1255
1331
  if (process.env.NW_NOTIFICATIONS) {
1256
1332
  try {
1257
1333
  const parsed = JSON.parse(process.env.NW_NOTIFICATIONS);
@@ -1325,7 +1401,7 @@ function buildEnvOverrideConfig(fileConfig) {
1325
1401
  env.claudeModel = model;
1326
1402
  }
1327
1403
  }
1328
- for (const jobId of ["qa", "audit", "analytics", "manager"]) {
1404
+ for (const jobId of ["qa", "audit", "ux", "analytics", "manager"]) {
1329
1405
  const jobDef = getJobDef(jobId);
1330
1406
  if (!jobDef)
1331
1407
  continue;
@@ -1438,6 +1514,8 @@ function getDefaultConfig() {
1438
1514
  executorEnabled: DEFAULT_EXECUTOR_ENABLED,
1439
1515
  reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
1440
1516
  providerEnv: { ...DEFAULT_PROVIDER_ENV },
1517
+ modelAttribution: DEFAULT_MODEL_ATTRIBUTION,
1518
+ newPrLabel: DEFAULT_NEW_PR_LABEL,
1441
1519
  notifications: { ...DEFAULT_NOTIFICATIONS, webhooks: [] },
1442
1520
  prdPriority: [...DEFAULT_PRD_PRIORITY],
1443
1521
  roadmapScanner: { ...DEFAULT_ROADMAP_SCANNER },
@@ -1451,6 +1529,7 @@ function getDefaultConfig() {
1451
1529
  claudeModel: DEFAULT_CLAUDE_MODEL,
1452
1530
  qa: { ...DEFAULT_QA },
1453
1531
  audit: { ...DEFAULT_AUDIT },
1532
+ ux: { ...DEFAULT_UX, flows: [...DEFAULT_UX.flows] },
1454
1533
  analytics: { ...DEFAULT_ANALYTICS },
1455
1534
  manager: { ...DEFAULT_MANAGER },
1456
1535
  feedback: { ...DEFAULT_FEEDBACK },
@@ -1652,7 +1731,7 @@ function mergeConfigLayer(base, layer) {
1652
1731
  })
1653
1732
  }
1654
1733
  };
1655
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "manager" || _key === "feedback" || _key === "prResolver" || _key === "merger") {
1734
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "ux" || _key === "analytics" || _key === "manager" || _key === "feedback" || _key === "prResolver" || _key === "merger") {
1656
1735
  base[_key] = {
1657
1736
  ...base[_key],
1658
1737
  ...value
@@ -4549,6 +4628,9 @@ function qaLockPath(projectDir) {
4549
4628
  function auditLockPath(projectDir) {
4550
4629
  return `${LOCK_FILE_PREFIX}audit-${projectRuntimeKey(projectDir)}.lock`;
4551
4630
  }
4631
+ function uxLockPath(projectDir) {
4632
+ return `${LOCK_FILE_PREFIX}ux-${projectRuntimeKey(projectDir)}.lock`;
4633
+ }
4552
4634
  function plannerLockPath(projectDir) {
4553
4635
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
4554
4636
  }
@@ -4951,6 +5033,7 @@ function collectLogInfo(projectDir) {
4951
5033
  { name: "reviewer", fileName: "reviewer.log" },
4952
5034
  { name: "qa", fileName: `${QA_LOG_NAME}.log` },
4953
5035
  { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
5036
+ { name: "ux", fileName: `${UX_LOG_NAME}.log` },
4954
5037
  { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
4955
5038
  { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` },
4956
5039
  { name: "pr-resolver", fileName: `${PR_RESOLVER_LOG_NAME}.log` },
@@ -4983,6 +5066,7 @@ async function fetchStatusSnapshot(projectDir, config) {
4983
5066
  const reviewerLock = checkLockFile(reviewerLockPath(projectDir));
4984
5067
  const qaLock = checkLockFile(qaLockPath(projectDir));
4985
5068
  const auditLock = checkLockFile(auditLockPath(projectDir));
5069
+ const uxLock = checkLockFile(uxLockPath(projectDir));
4986
5070
  const plannerLock = checkLockFile(plannerLockPath(projectDir));
4987
5071
  const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
4988
5072
  const prResolverLock = checkLockFile(prResolverLockPath(projectDir));
@@ -4993,6 +5077,7 @@ async function fetchStatusSnapshot(projectDir, config) {
4993
5077
  { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
4994
5078
  { name: "qa", running: qaLock.running, pid: qaLock.pid },
4995
5079
  { name: "audit", running: auditLock.running, pid: auditLock.pid },
5080
+ { name: "ux", running: uxLock.running, pid: uxLock.pid },
4996
5081
  { name: "planner", running: plannerLock.running, pid: plannerLock.pid },
4997
5082
  { name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid },
4998
5083
  { name: "pr-resolver", running: prResolverLock.running, pid: prResolverLock.pid },
@@ -7999,6 +8084,8 @@ function getLockPathForJob(projectPath, jobType) {
7999
8084
  return qaLockPath(projectPath);
8000
8085
  case "audit":
8001
8086
  return auditLockPath(projectPath);
8087
+ case "ux":
8088
+ return uxLockPath(projectPath);
8002
8089
  case "slicer":
8003
8090
  case "planner":
8004
8091
  return plannerLockPath(projectPath);
@@ -8866,6 +8953,351 @@ var init_analytics = __esm({
8866
8953
  }
8867
8954
  });
8868
8955
 
8956
+ // ../core/dist/telemetry/amplitude-ingest-client.js
8957
+ var AmplitudeIngestClient;
8958
+ var init_amplitude_ingest_client = __esm({
8959
+ "../core/dist/telemetry/amplitude-ingest-client.js"() {
8960
+ "use strict";
8961
+ init_constants();
8962
+ AmplitudeIngestClient = class {
8963
+ apiKey;
8964
+ endpoint;
8965
+ fetchImpl;
8966
+ timeoutMs;
8967
+ constructor(options) {
8968
+ this.apiKey = options.apiKey;
8969
+ this.endpoint = options.endpoint ?? DEFAULT_AMPLITUDE_INGEST_ENDPOINT;
8970
+ this.fetchImpl = options.fetchImpl ?? fetch;
8971
+ this.timeoutMs = options.timeoutMs ?? 1500;
8972
+ }
8973
+ async send(event) {
8974
+ const controller = new AbortController();
8975
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
8976
+ try {
8977
+ const response = await this.fetchImpl(this.endpoint, {
8978
+ method: "POST",
8979
+ headers: { "content-type": "application/json" },
8980
+ body: JSON.stringify({
8981
+ api_key: this.apiKey,
8982
+ events: [
8983
+ {
8984
+ device_id: event.installId,
8985
+ event_type: event.eventName,
8986
+ event_properties: event.properties,
8987
+ time: event.time ?? Date.now()
8988
+ }
8989
+ ]
8990
+ }),
8991
+ signal: controller.signal
8992
+ });
8993
+ if (!response.ok) {
8994
+ throw new Error(`Amplitude ingestion failed with ${response.status}`);
8995
+ }
8996
+ } finally {
8997
+ clearTimeout(timeout);
8998
+ }
8999
+ }
9000
+ };
9001
+ }
9002
+ });
9003
+
9004
+ // ../core/dist/telemetry/config.js
9005
+ import * as crypto2 from "crypto";
9006
+ import * as fs23 from "fs";
9007
+ import * as os9 from "os";
9008
+ import * as path22 from "path";
9009
+ function getTelemetryConfigPath(env = process.env) {
9010
+ const base = env.NIGHT_WATCH_HOME || path22.join(os9.homedir(), GLOBAL_CONFIG_DIR);
9011
+ return path22.join(base, TELEMETRY_FILE_NAME);
9012
+ }
9013
+ function isTelemetryEnvDisabled(env = process.env) {
9014
+ if (env.NW_TELEMETRY_DISABLED === "1") {
9015
+ return "env:NW_TELEMETRY_DISABLED";
9016
+ }
9017
+ if (env.DO_NOT_TRACK === "1") {
9018
+ return "env:DO_NOT_TRACK";
9019
+ }
9020
+ return null;
9021
+ }
9022
+ function createTelemetryConfig(now) {
9023
+ const timestamp = now.toISOString();
9024
+ return {
9025
+ schemaVersion: 1,
9026
+ installId: crypto2.randomUUID(),
9027
+ enabled: true,
9028
+ createdAt: timestamp,
9029
+ updatedAt: timestamp
9030
+ };
9031
+ }
9032
+ function parseTelemetryConfig(raw) {
9033
+ const parsed = JSON.parse(raw);
9034
+ if (parsed.schemaVersion !== 1 || typeof parsed.installId !== "string" || parsed.installId.length === 0 || typeof parsed.enabled !== "boolean" || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") {
9035
+ return null;
9036
+ }
9037
+ return {
9038
+ schemaVersion: 1,
9039
+ installId: parsed.installId,
9040
+ enabled: parsed.enabled,
9041
+ noticeShownAt: typeof parsed.noticeShownAt === "string" && parsed.noticeShownAt.length > 0 ? parsed.noticeShownAt : void 0,
9042
+ createdAt: parsed.createdAt,
9043
+ updatedAt: parsed.updatedAt
9044
+ };
9045
+ }
9046
+ function saveTelemetryConfig(config, env = process.env) {
9047
+ const filePath = getTelemetryConfigPath(env);
9048
+ fs23.mkdirSync(path22.dirname(filePath), { recursive: true });
9049
+ fs23.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
9050
+ }
9051
+ function loadOrCreateTelemetryConfig(options = {}) {
9052
+ const env = options.env ?? process.env;
9053
+ const now = options.now?.() ?? /* @__PURE__ */ new Date();
9054
+ const filePath = getTelemetryConfigPath(env);
9055
+ try {
9056
+ if (fs23.existsSync(filePath)) {
9057
+ const parsed = parseTelemetryConfig(fs23.readFileSync(filePath, "utf-8"));
9058
+ if (parsed) {
9059
+ return parsed;
9060
+ }
9061
+ }
9062
+ } catch {
9063
+ }
9064
+ const fresh = createTelemetryConfig(now);
9065
+ try {
9066
+ saveTelemetryConfig(fresh, env);
9067
+ } catch {
9068
+ }
9069
+ return fresh;
9070
+ }
9071
+ function getTelemetryEffectiveState(options = {}) {
9072
+ const env = options.env ?? process.env;
9073
+ const config = loadOrCreateTelemetryConfig(options);
9074
+ const envDisabled = isTelemetryEnvDisabled(env);
9075
+ const pathValue = getTelemetryConfigPath(env);
9076
+ if (envDisabled) {
9077
+ return { config, enabled: false, path: pathValue, reason: envDisabled };
9078
+ }
9079
+ if (!config.enabled) {
9080
+ return { config, enabled: false, path: pathValue, reason: "config" };
9081
+ }
9082
+ if (options.apiKey !== void 0 && options.apiKey.trim().length === 0) {
9083
+ return { config, enabled: false, path: pathValue, reason: "missing-api-key" };
9084
+ }
9085
+ return { config, enabled: true, path: pathValue, reason: "config" };
9086
+ }
9087
+ function setTelemetryEnabled(enabled, options = {}) {
9088
+ const env = options.env ?? process.env;
9089
+ const config = loadOrCreateTelemetryConfig(options);
9090
+ const updated = {
9091
+ ...config,
9092
+ enabled,
9093
+ updatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
9094
+ };
9095
+ saveTelemetryConfig(updated, env);
9096
+ return updated;
9097
+ }
9098
+ function markTelemetryNoticeShown(options = {}) {
9099
+ const env = options.env ?? process.env;
9100
+ const config = loadOrCreateTelemetryConfig(options);
9101
+ const timestamp = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
9102
+ const updated = {
9103
+ ...config,
9104
+ noticeShownAt: config.noticeShownAt ?? timestamp,
9105
+ updatedAt: timestamp
9106
+ };
9107
+ saveTelemetryConfig(updated, env);
9108
+ return updated;
9109
+ }
9110
+ var init_config2 = __esm({
9111
+ "../core/dist/telemetry/config.js"() {
9112
+ "use strict";
9113
+ init_constants();
9114
+ }
9115
+ });
9116
+
9117
+ // ../core/dist/telemetry/schema.js
9118
+ var TELEMETRY_EVENT_NAMES, TELEMETRY_ERROR_CATEGORIES;
9119
+ var init_schema = __esm({
9120
+ "../core/dist/telemetry/schema.js"() {
9121
+ "use strict";
9122
+ TELEMETRY_EVENT_NAMES = [
9123
+ "cli_first_run",
9124
+ "cli_init_completed",
9125
+ "command_started",
9126
+ "command_completed",
9127
+ "job_started",
9128
+ "job_completed",
9129
+ "job_failed",
9130
+ "pr_opened",
9131
+ "review_completed",
9132
+ "auto_merge_completed",
9133
+ "doctor_failed",
9134
+ "telemetry_enabled",
9135
+ "telemetry_disabled"
9136
+ ];
9137
+ TELEMETRY_ERROR_CATEGORIES = [
9138
+ "config",
9139
+ "provider",
9140
+ "github",
9141
+ "network",
9142
+ "rate_limit",
9143
+ "timeout",
9144
+ "validation",
9145
+ "unknown"
9146
+ ];
9147
+ }
9148
+ });
9149
+
9150
+ // ../core/dist/telemetry/sanitizer.js
9151
+ function isAllowedTelemetryEventName(eventName) {
9152
+ return TELEMETRY_EVENT_NAMES.includes(eventName);
9153
+ }
9154
+ function isSuspiciousString(value) {
9155
+ if (!SAFE_STRING_PATTERN.test(value)) {
9156
+ return true;
9157
+ }
9158
+ return value.includes("/") || value.includes("\\") || /^[a-z][a-z0-9+.-]*:/i.test(value);
9159
+ }
9160
+ function sanitizeString(value) {
9161
+ if (typeof value !== "string") {
9162
+ return void 0;
9163
+ }
9164
+ const trimmed = value.trim();
9165
+ if (trimmed.length === 0 || isSuspiciousString(trimmed)) {
9166
+ return void 0;
9167
+ }
9168
+ return trimmed;
9169
+ }
9170
+ function sanitizeBoolean(value) {
9171
+ return typeof value === "boolean" ? value : void 0;
9172
+ }
9173
+ function sanitizeInteger(value) {
9174
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
9175
+ return void 0;
9176
+ }
9177
+ return value;
9178
+ }
9179
+ function mapTelemetryErrorCategory(value) {
9180
+ if (typeof value === "string") {
9181
+ const normalized = value.toLowerCase();
9182
+ if (TELEMETRY_ERROR_CATEGORIES.includes(normalized)) {
9183
+ return normalized;
9184
+ }
9185
+ if (normalized.includes("timeout") || normalized.includes("timed out"))
9186
+ return "timeout";
9187
+ if (normalized.includes("rate"))
9188
+ return "rate_limit";
9189
+ if (normalized.includes("github") || normalized.includes("gh "))
9190
+ return "github";
9191
+ if (normalized.includes("network") || normalized.includes("fetch"))
9192
+ return "network";
9193
+ if (normalized.includes("provider") || normalized.includes("claude") || normalized.includes("codex")) {
9194
+ return "provider";
9195
+ }
9196
+ if (normalized.includes("config"))
9197
+ return "config";
9198
+ if (normalized.includes("valid"))
9199
+ return "validation";
9200
+ }
9201
+ return "unknown";
9202
+ }
9203
+ function sanitizeTelemetryEvent(eventName, properties = {}) {
9204
+ if (!isAllowedTelemetryEventName(eventName)) {
9205
+ return null;
9206
+ }
9207
+ const sanitized = {};
9208
+ for (const key of STRING_PROPERTIES) {
9209
+ const value = sanitizeString(properties[key]);
9210
+ if (value !== void 0) {
9211
+ sanitized[key] = value;
9212
+ }
9213
+ }
9214
+ for (const key of BOOLEAN_PROPERTIES) {
9215
+ const value = sanitizeBoolean(properties[key]);
9216
+ if (value !== void 0) {
9217
+ sanitized[key] = value;
9218
+ }
9219
+ }
9220
+ for (const key of INTEGER_PROPERTIES) {
9221
+ const value = sanitizeInteger(properties[key]);
9222
+ if (value !== void 0) {
9223
+ sanitized[key] = value;
9224
+ }
9225
+ }
9226
+ if (properties.errorCategory !== void 0) {
9227
+ sanitized.errorCategory = mapTelemetryErrorCategory(properties.errorCategory);
9228
+ }
9229
+ return { eventName, properties: sanitized };
9230
+ }
9231
+ var STRING_PROPERTIES, BOOLEAN_PROPERTIES, INTEGER_PROPERTIES, SAFE_STRING_PATTERN;
9232
+ var init_sanitizer = __esm({
9233
+ "../core/dist/telemetry/sanitizer.js"() {
9234
+ "use strict";
9235
+ init_schema();
9236
+ STRING_PROPERTIES = ["cliVersion", "command", "jobType", "provider", "platform"];
9237
+ BOOLEAN_PROPERTIES = ["success", "failure", "boardMode"];
9238
+ INTEGER_PROPERTIES = [
9239
+ "durationMs",
9240
+ "exitCode",
9241
+ "nodeMajorVersion",
9242
+ "registeredProjectCount"
9243
+ ];
9244
+ SAFE_STRING_PATTERN = /^[a-zA-Z0-9_.:-]{1,80}$/;
9245
+ }
9246
+ });
9247
+
9248
+ // ../core/dist/telemetry/reporter.js
9249
+ function resolveAmplitudeApiKey(env, explicit) {
9250
+ return explicit ?? env.NW_AMPLITUDE_API_KEY ?? DEFAULT_AMPLITUDE_API_KEY;
9251
+ }
9252
+ async function trackTelemetryEvent(eventName, properties = {}, options = {}) {
9253
+ const env = options.env ?? process.env;
9254
+ const apiKey = resolveAmplitudeApiKey(env, options.apiKey);
9255
+ const state = getTelemetryEffectiveState({ env, apiKey, now: options.now });
9256
+ if (!state.enabled) {
9257
+ return { sent: false, reason: `disabled:${state.reason}` };
9258
+ }
9259
+ if (apiKey.trim().length === 0) {
9260
+ return { sent: false, reason: "missing-api-key" };
9261
+ }
9262
+ const sanitized = sanitizeTelemetryEvent(eventName, properties);
9263
+ if (!sanitized) {
9264
+ return { sent: false, reason: "dropped:event-name" };
9265
+ }
9266
+ try {
9267
+ const client = options.client ?? new AmplitudeIngestClient({ apiKey });
9268
+ await client.send({
9269
+ eventName: sanitized.eventName,
9270
+ installId: state.config.installId,
9271
+ properties: sanitized.properties,
9272
+ time: options.now?.().getTime()
9273
+ });
9274
+ return { sent: true };
9275
+ } catch {
9276
+ return { sent: false, reason: "network-error" };
9277
+ }
9278
+ }
9279
+ var init_reporter = __esm({
9280
+ "../core/dist/telemetry/reporter.js"() {
9281
+ "use strict";
9282
+ init_constants();
9283
+ init_amplitude_ingest_client();
9284
+ init_config2();
9285
+ init_sanitizer();
9286
+ }
9287
+ });
9288
+
9289
+ // ../core/dist/telemetry/index.js
9290
+ var init_telemetry = __esm({
9291
+ "../core/dist/telemetry/index.js"() {
9292
+ "use strict";
9293
+ init_amplitude_ingest_client();
9294
+ init_config2();
9295
+ init_reporter();
9296
+ init_sanitizer();
9297
+ init_schema();
9298
+ }
9299
+ });
9300
+
8869
9301
  // ../core/dist/audit/board-sync.js
8870
9302
  function humanizeCategory(category) {
8871
9303
  return category.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
@@ -9030,19 +9462,272 @@ var init_audit = __esm({
9030
9462
  }
9031
9463
  });
9032
9464
 
9465
+ // ../core/dist/ux/ux-runner.js
9466
+ import * as fs24 from "fs";
9467
+ import * as os10 from "os";
9468
+ import * as path23 from "path";
9469
+ function asString(value, fallback = "") {
9470
+ return typeof value === "string" ? value.trim() : fallback;
9471
+ }
9472
+ function asStringArray(value) {
9473
+ if (Array.isArray(value)) {
9474
+ return value.map((item) => asString(item)).filter(Boolean);
9475
+ }
9476
+ const single = asString(value);
9477
+ return single ? [single] : [];
9478
+ }
9479
+ function normalizeUxPriority(value) {
9480
+ const raw = asString(value).toUpperCase();
9481
+ if (raw === "P0" || raw === "CRITICAL" || raw === "BLOCKER")
9482
+ return "P0";
9483
+ if (raw === "P1" || raw === "HIGH" || raw === "MAJOR")
9484
+ return "P1";
9485
+ if (raw === "P2" || raw === "MEDIUM" || raw === "MODERATE")
9486
+ return "P2";
9487
+ return "P3";
9488
+ }
9489
+ function sortUxFindings(findings) {
9490
+ return [...findings].sort((left, right) => PRIORITY_ORDER.indexOf(left.priority) - PRIORITY_ORDER.indexOf(right.priority) || left.title.localeCompare(right.title));
9491
+ }
9492
+ function rawFindingToFinding(raw) {
9493
+ const title = asString(raw.title);
9494
+ if (!title)
9495
+ return null;
9496
+ return {
9497
+ title,
9498
+ priority: normalizeUxPriority(raw.priority ?? raw.severity),
9499
+ impact: asString(raw.impact, "Not specified"),
9500
+ affectedFlows: asStringArray(raw.affectedFlows ?? raw.affectedFlow ?? raw.flows),
9501
+ affectedPages: asStringArray(raw.affectedPages ?? raw.affectedPage ?? raw.pages),
9502
+ evidence: asStringArray(raw.evidence),
9503
+ reproductionSteps: asStringArray(raw.reproductionSteps ?? raw.steps),
9504
+ recommendedFix: asString(raw.recommendedFix ?? raw.recommendation ?? raw.fix, "Investigate and improve the affected user experience.")
9505
+ };
9506
+ }
9507
+ function extractJsonCandidate(text) {
9508
+ const trimmed = text.trim();
9509
+ try {
9510
+ return JSON.parse(trimmed);
9511
+ } catch {
9512
+ const arrayStart = text.indexOf("[");
9513
+ const arrayEnd = text.lastIndexOf("]");
9514
+ if (arrayStart !== -1 && arrayEnd > arrayStart) {
9515
+ return JSON.parse(text.slice(arrayStart, arrayEnd + 1));
9516
+ }
9517
+ const objectStart = text.indexOf("{");
9518
+ const objectEnd = text.lastIndexOf("}");
9519
+ if (objectStart !== -1 && objectEnd > objectStart) {
9520
+ return JSON.parse(text.slice(objectStart, objectEnd + 1));
9521
+ }
9522
+ throw new Error("No JSON object or array found in UX provider output");
9523
+ }
9524
+ }
9525
+ function parseUxFindings(text) {
9526
+ let parsed;
9527
+ try {
9528
+ parsed = extractJsonCandidate(text);
9529
+ } catch {
9530
+ logger6.warn("Failed to parse UX provider output as JSON");
9531
+ return [];
9532
+ }
9533
+ let rawItems = [];
9534
+ if (Array.isArray(parsed)) {
9535
+ rawItems = parsed;
9536
+ } else if (Array.isArray(parsed?.findings)) {
9537
+ rawItems = parsed.findings;
9538
+ }
9539
+ const findings = rawItems.filter((item) => item !== null && typeof item === "object").map(rawFindingToFinding).filter((item) => item !== null);
9540
+ return sortUxFindings(findings);
9541
+ }
9542
+ function formatList(items, fallback = "Not specified") {
9543
+ if (items.length === 0)
9544
+ return fallback;
9545
+ return items.map((item) => `- ${item}`).join("\n");
9546
+ }
9547
+ function buildUxReportBody(input) {
9548
+ const lines = [
9549
+ "# UX Report",
9550
+ "",
9551
+ `Generated by Night Watch UX agent on ${(/* @__PURE__ */ new Date()).toISOString()}.`,
9552
+ "",
9553
+ "## Scope",
9554
+ "",
9555
+ `- Base URL: ${input.baseUrl || "Not configured"}`,
9556
+ `- Start URL: ${input.startUrl || "Not configured"}`,
9557
+ "- Configured flows:",
9558
+ input.flows.length > 0 ? formatList(input.flows) : "- Not configured",
9559
+ "",
9560
+ "## Findings"
9561
+ ];
9562
+ input.findings.forEach((finding, index) => {
9563
+ lines.push("", `### ${index + 1}. [${finding.priority}] ${finding.title}`, "", "**Impact**", "", finding.impact, "", "**Affected Flows**", "", formatList(finding.affectedFlows), "", "**Affected Pages**", "", formatList(finding.affectedPages), "", "**Evidence**", "", formatList(finding.evidence), "", "**Reproduction Steps**", "", formatList(finding.reproductionSteps), "", "**Recommended Fix**", "", finding.recommendedFix);
9564
+ });
9565
+ return lines.join("\n");
9566
+ }
9567
+ function buildUxAuditPrompt(config, projectDir) {
9568
+ const ux = config.ux;
9569
+ const configuredUrl = ux.startUrl || ux.baseUrl;
9570
+ const custom = ux.reportPrompt.trim();
9571
+ return [
9572
+ custom || "You are a senior UX reviewer creating an actionable UX report.",
9573
+ "",
9574
+ "Use Playwright/browser automation for the inspection. If Playwright is missing and auto-install is enabled, use a lightweight local check/install such as `npx playwright --version` and `npx playwright install chromium` as needed.",
9575
+ "Inspect the configured app, traverse the listed flows, capture screenshots or other evidence when useful, and focus on issues that materially affect user comprehension, completion, accessibility, responsiveness, or trust.",
9576
+ "",
9577
+ "Return only JSON. Use this exact shape:",
9578
+ '[{"title":"...","priority":"P0|P1|P2|P3","impact":"...","affectedFlows":["..."],"affectedPages":["..."],"evidence":["screenshot path or observation"],"reproductionSteps":["..."],"recommendedFix":"..."}]',
9579
+ "If no actionable UX issues are found, return [] only.",
9580
+ "",
9581
+ "Priority definitions:",
9582
+ "- P0: blocks core user flow or causes serious data/action risk",
9583
+ "- P1: major friction, broken responsive state, or accessibility failure in a key flow",
9584
+ "- P2: moderate confusion or inefficient interaction",
9585
+ "- P3: polish or minor usability concern",
9586
+ "",
9587
+ `Project directory: ${projectDir}`,
9588
+ `Base URL: ${ux.baseUrl || "not configured"}`,
9589
+ `Start URL: ${configuredUrl || "not configured"}`,
9590
+ `Auto-install Playwright: ${ux.autoInstallPlaywright ? "yes" : "no"}`,
9591
+ `Maximum findings: ${Math.max(1, ux.maxIssues)}`,
9592
+ "",
9593
+ "Configured flows:",
9594
+ ux.flows.length > 0 ? ux.flows.map((flow) => `- ${flow}`).join("\n") : "- Discover the primary user flows from the app/repo."
9595
+ ].join("\n");
9596
+ }
9597
+ function shellQuote(value) {
9598
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
9599
+ }
9600
+ function buildPresetCommand(preset, promptFile, projectDir) {
9601
+ const args = [preset.command];
9602
+ if (preset.subcommand)
9603
+ args.push(preset.subcommand);
9604
+ if (preset.workdirFlag)
9605
+ args.push(preset.workdirFlag, projectDir);
9606
+ if (preset.modelFlag && preset.model)
9607
+ args.push(preset.modelFlag, preset.model);
9608
+ if (preset.autoApproveFlag)
9609
+ args.push(preset.autoApproveFlag);
9610
+ const quotedPrefix = args.map(shellQuote).join(" ");
9611
+ const promptArg = `"$(cat ${shellQuote(promptFile)})"`;
9612
+ if (preset.promptFlag) {
9613
+ return `${quotedPrefix} ${shellQuote(preset.promptFlag)} ${promptArg}`;
9614
+ }
9615
+ return `${quotedPrefix} ${promptArg}`;
9616
+ }
9617
+ async function invokeProvider(prompt, config, projectDir) {
9618
+ const tmpDir = fs24.mkdtempSync(path23.join(os10.tmpdir(), "nw-ux-"));
9619
+ const promptFile = path23.join(tmpDir, "ux-prompt.md");
9620
+ const scriptFile = path23.join(tmpDir, "run-ux.sh");
9621
+ try {
9622
+ fs24.writeFileSync(promptFile, prompt, "utf-8");
9623
+ const provider = resolveJobProvider(config, "ux");
9624
+ const preset = resolvePreset(config, provider);
9625
+ const command = buildPresetCommand(preset, promptFile, projectDir);
9626
+ fs24.writeFileSync(scriptFile, `#!/usr/bin/env bash
9627
+ set -euo pipefail
9628
+ ${command} 2>&1
9629
+ `, {
9630
+ mode: 493
9631
+ });
9632
+ const env = { ...preset.envVars ?? {}, ...config.providerEnv ?? {} };
9633
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], env, {
9634
+ cwd: projectDir
9635
+ });
9636
+ if (exitCode !== 0) {
9637
+ throw new Error(`UX provider exited with code ${exitCode}: ${stderr || stdout}`);
9638
+ }
9639
+ return `${stdout}
9640
+ ${stderr}`;
9641
+ } finally {
9642
+ try {
9643
+ fs24.rmSync(tmpDir, { recursive: true, force: true });
9644
+ } catch {
9645
+ }
9646
+ }
9647
+ }
9648
+ async function resolveBoardProvider(config, projectDir, injected) {
9649
+ if (injected !== void 0)
9650
+ return injected;
9651
+ if (!config.boardProvider?.enabled)
9652
+ return null;
9653
+ return createBoardProvider(config.boardProvider, projectDir);
9654
+ }
9655
+ async function runUx(config, projectDir, options = {}) {
9656
+ const prompt = buildUxAuditPrompt(config, projectDir);
9657
+ const providerOutput = options.providerOutput ?? await (options.providerInvoker ?? invokeProvider)(prompt, config, projectDir);
9658
+ const findings = parseUxFindings(providerOutput).slice(0, Math.max(1, config.ux.maxIssues));
9659
+ if (findings.length === 0) {
9660
+ return { findings, issuesCreated: 0, summary: "No actionable UX issues found" };
9661
+ }
9662
+ const body = buildUxReportBody({
9663
+ findings,
9664
+ baseUrl: config.ux.baseUrl,
9665
+ startUrl: config.ux.startUrl,
9666
+ flows: config.ux.flows
9667
+ });
9668
+ if (options.dryRun) {
9669
+ return {
9670
+ findings,
9671
+ issuesCreated: 0,
9672
+ summary: `Dry run found ${findings.length} UX finding(s); no board report created`
9673
+ };
9674
+ }
9675
+ const boardProvider = await resolveBoardProvider(config, projectDir, options.boardProvider);
9676
+ if (!boardProvider) {
9677
+ return {
9678
+ findings,
9679
+ issuesCreated: 0,
9680
+ summary: `Found ${findings.length} UX finding(s); board provider is disabled`
9681
+ };
9682
+ }
9683
+ const targetColumn = config.ux.targetColumn;
9684
+ const issue = await boardProvider.createIssue({
9685
+ title: `UX Report: ${findings.length} prioritized finding(s)`,
9686
+ body,
9687
+ column: targetColumn,
9688
+ labels: ["ux", "night-watch"]
9689
+ });
9690
+ return {
9691
+ findings,
9692
+ issuesCreated: 1,
9693
+ reportUrl: issue.url,
9694
+ summary: `Created UX report with ${findings.length} finding(s) in ${targetColumn}`
9695
+ };
9696
+ }
9697
+ var logger6, PRIORITY_ORDER;
9698
+ var init_ux_runner = __esm({
9699
+ "../core/dist/ux/ux-runner.js"() {
9700
+ "use strict";
9701
+ init_factory();
9702
+ init_config();
9703
+ init_shell();
9704
+ init_logger();
9705
+ logger6 = createLogger("ux");
9706
+ PRIORITY_ORDER = ["P0", "P1", "P2", "P3"];
9707
+ }
9708
+ });
9709
+
9710
+ // ../core/dist/ux/index.js
9711
+ var init_ux = __esm({
9712
+ "../core/dist/ux/index.js"() {
9713
+ "use strict";
9714
+ init_ux_runner();
9715
+ }
9716
+ });
9717
+
9033
9718
  // ../core/dist/manager/manager-memory.js
9034
- import * as fs23 from "fs";
9035
- import * as path22 from "path";
9719
+ import * as fs25 from "fs";
9720
+ import * as path24 from "path";
9036
9721
  import { createHash as createHash4 } from "crypto";
9037
9722
  function createFindingFingerprint(parts) {
9038
9723
  const normalized = parts.map((part) => part.toLowerCase().trim().replace(/\s+/g, " ")).join("|");
9039
9724
  return createHash4("sha256").update(normalized).digest("hex").slice(0, 16);
9040
9725
  }
9041
9726
  function loadManagerMemory(memoryPath) {
9042
- if (!fs23.existsSync(memoryPath)) {
9727
+ if (!fs25.existsSync(memoryPath)) {
9043
9728
  return { fingerprints: /* @__PURE__ */ new Set(), lastWeeklySummaryAt: null, raw: "" };
9044
9729
  }
9045
- const raw = fs23.readFileSync(memoryPath, "utf-8");
9730
+ const raw = fs25.readFileSync(memoryPath, "utf-8");
9046
9731
  const fingerprints = /* @__PURE__ */ new Set();
9047
9732
  let match;
9048
9733
  while ((match = FINGERPRINT_PATTERN.exec(raw)) !== null) {
@@ -9100,8 +9785,8 @@ function renderManagerMemory(result, previous) {
9100
9785
  `;
9101
9786
  }
9102
9787
  function writeManagerMemory(memoryPath, result, previous) {
9103
- fs23.mkdirSync(path22.dirname(memoryPath), { recursive: true });
9104
- fs23.writeFileSync(memoryPath, renderManagerMemory(result, previous), "utf-8");
9788
+ fs25.mkdirSync(path24.dirname(memoryPath), { recursive: true });
9789
+ fs25.writeFileSync(memoryPath, renderManagerMemory(result, previous), "utf-8");
9105
9790
  }
9106
9791
  function renderFinding(finding) {
9107
9792
  return [
@@ -9149,13 +9834,13 @@ var init_manager_memory = __esm({
9149
9834
  });
9150
9835
 
9151
9836
  // ../core/dist/manager/manager-analysis.js
9152
- import * as fs24 from "fs";
9153
- import * as path23 from "path";
9837
+ import * as fs26 from "fs";
9838
+ import * as path25 from "path";
9154
9839
  function analyzeManagerInputs(context) {
9155
- const roadmapPath = path23.resolve(context.projectDir, context.config.roadmapScanner?.roadmapPath || "ROADMAP.md");
9156
- const roadmapContent = fs24.existsSync(roadmapPath) ? fs24.readFileSync(roadmapPath, "utf-8") : "";
9840
+ const roadmapPath = path25.resolve(context.projectDir, context.config.roadmapScanner?.roadmapPath || "ROADMAP.md");
9841
+ const roadmapContent = fs26.existsSync(roadmapPath) ? fs26.readFileSync(roadmapPath, "utf-8") : "";
9157
9842
  const roadmapItems = roadmapContent ? parseRoadmap(roadmapContent) : [];
9158
- const prds = collectPrdFiles(path23.resolve(context.projectDir, context.config.prdDir || "docs/prds"));
9843
+ const prds = collectPrdFiles(path25.resolve(context.projectDir, context.config.prdDir || "docs/prds"));
9159
9844
  const findings = [];
9160
9845
  const searchableWork = buildSearchableWork(context.boardIssues.map((issue) => issue.title), prds);
9161
9846
  for (const item of roadmapItems.filter((roadmapItem) => !roadmapItem.checked)) {
@@ -9203,7 +9888,7 @@ function analyzeManagerInputs(context) {
9203
9888
  labels: ["manager", "queue", "blocked"]
9204
9889
  });
9205
9890
  }
9206
- if (!fs24.existsSync(path23.join(context.managerConfig.docsDirectory, "overview.md"))) {
9891
+ if (!fs26.existsSync(path25.join(context.managerConfig.docsDirectory, "overview.md"))) {
9207
9892
  const fingerprint = createFindingFingerprint(["missing_manager_doc", context.managerConfig.docsDirectory]);
9208
9893
  findings.push({
9209
9894
  kind: "missing_manager_doc",
@@ -9219,19 +9904,19 @@ function analyzeManagerInputs(context) {
9219
9904
  return { findings, roadmapItems: roadmapItems.length, prds };
9220
9905
  }
9221
9906
  function collectPrdFiles(prdDir) {
9222
- if (!fs24.existsSync(prdDir))
9907
+ if (!fs26.existsSync(prdDir))
9223
9908
  return [];
9224
9909
  const files = [];
9225
9910
  const visit = (dir) => {
9226
- for (const entry of fs24.readdirSync(dir, { withFileTypes: true })) {
9227
- const fullPath = path23.join(dir, entry.name);
9911
+ for (const entry of fs26.readdirSync(dir, { withFileTypes: true })) {
9912
+ const fullPath = path25.join(dir, entry.name);
9228
9913
  if (entry.isDirectory()) {
9229
9914
  visit(fullPath);
9230
9915
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
9231
9916
  files.push({
9232
9917
  name: entry.name.replace(/\.md$/, ""),
9233
9918
  path: fullPath,
9234
- content: fs24.readFileSync(fullPath, "utf-8")
9919
+ content: fs26.readFileSync(fullPath, "utf-8")
9235
9920
  });
9236
9921
  }
9237
9922
  }
@@ -9406,14 +10091,14 @@ var init_manager_notifications = __esm({
9406
10091
  });
9407
10092
 
9408
10093
  // ../core/dist/manager/manager-runner.js
9409
- import * as fs25 from "fs";
9410
- import * as path24 from "path";
10094
+ import * as fs27 from "fs";
10095
+ import * as path26 from "path";
9411
10096
  async function runManager(projectDir, config, options = {}) {
9412
10097
  const dryRun = options.dryRun ?? false;
9413
10098
  const now = options.now ?? /* @__PURE__ */ new Date();
9414
10099
  const managerConfig = resolveManagerConfig(config, projectDir);
9415
10100
  const memory = loadManagerMemory(managerConfig.memoryFile);
9416
- const boardProvider = await resolveBoardProvider(config, projectDir, options.boardProvider);
10101
+ const boardProvider = await resolveBoardProvider2(config, projectDir, options.boardProvider);
9417
10102
  const boardIssues = await readBoardIssues(boardProvider);
9418
10103
  const queueStatus = resolveQueueStatus(options.queueStatus);
9419
10104
  const statusSnapshot = await resolveStatusSnapshot(projectDir, config, options.statusSnapshot);
@@ -9490,11 +10175,11 @@ function resolveManagerConfig(config, projectDir) {
9490
10175
  return {
9491
10176
  ...merged,
9492
10177
  weeklySummaryDay,
9493
- memoryFile: path24.resolve(projectDir, merged.memoryPath),
9494
- docsDirectory: path24.resolve(projectDir, merged.docsDir)
10178
+ memoryFile: path26.resolve(projectDir, merged.memoryPath),
10179
+ docsDirectory: path26.resolve(projectDir, merged.docsDir)
9495
10180
  };
9496
10181
  }
9497
- async function resolveBoardProvider(config, projectDir, injected) {
10182
+ async function resolveBoardProvider2(config, projectDir, injected) {
9498
10183
  if (injected !== void 0) {
9499
10184
  return injected;
9500
10185
  }
@@ -9533,13 +10218,13 @@ async function resolveStatusSnapshot(projectDir, config, injected) {
9533
10218
  }
9534
10219
  }
9535
10220
  function writeManagerDocs(docsDir, result, now) {
9536
- const overviewPath = path24.resolve(docsDir, "overview.md");
9537
- const relative3 = path24.relative(docsDir, overviewPath);
9538
- if (relative3.startsWith("..") || path24.isAbsolute(relative3)) {
10221
+ const overviewPath = path26.resolve(docsDir, "overview.md");
10222
+ const relative3 = path26.relative(docsDir, overviewPath);
10223
+ if (relative3.startsWith("..") || path26.isAbsolute(relative3)) {
9539
10224
  throw new Error(`Refusing to write Manager docs outside docsDir: ${overviewPath}`);
9540
10225
  }
9541
- fs25.mkdirSync(docsDir, { recursive: true });
9542
- fs25.writeFileSync(overviewPath, [
10226
+ fs27.mkdirSync(docsDir, { recursive: true });
10227
+ fs27.writeFileSync(overviewPath, [
9543
10228
  "# Manager Overview",
9544
10229
  "",
9545
10230
  `Generated: ${now.toISOString()}`,
@@ -10411,6 +11096,7 @@ var dist_exports = {};
10411
11096
  __export(dist_exports, {
10412
11097
  ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
10413
11098
  AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
11099
+ AmplitudeIngestClient: () => AmplitudeIngestClient,
10414
11100
  BOARD_COLUMNS: () => BOARD_COLUMNS,
10415
11101
  BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
10416
11102
  BUILT_IN_PRESET_IDS: () => BUILT_IN_PRESET_IDS,
@@ -10421,6 +11107,8 @@ __export(dist_exports, {
10421
11107
  CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
10422
11108
  CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
10423
11109
  DATABASE_TOKEN: () => DATABASE_TOKEN,
11110
+ DEFAULT_AMPLITUDE_API_KEY: () => DEFAULT_AMPLITUDE_API_KEY,
11111
+ DEFAULT_AMPLITUDE_INGEST_ENDPOINT: () => DEFAULT_AMPLITUDE_INGEST_ENDPOINT,
10424
11112
  DEFAULT_ANALYTICS: () => DEFAULT_ANALYTICS,
10425
11113
  DEFAULT_ANALYTICS_ENABLED: () => DEFAULT_ANALYTICS_ENABLED,
10426
11114
  DEFAULT_ANALYTICS_LOOKBACK_DAYS: () => DEFAULT_ANALYTICS_LOOKBACK_DAYS,
@@ -10473,6 +11161,8 @@ __export(dist_exports, {
10473
11161
  DEFAULT_MERGER_REBASE_BEFORE_MERGE: () => DEFAULT_MERGER_REBASE_BEFORE_MERGE,
10474
11162
  DEFAULT_MERGER_SCHEDULE: () => DEFAULT_MERGER_SCHEDULE,
10475
11163
  DEFAULT_MIN_REVIEW_SCORE: () => DEFAULT_MIN_REVIEW_SCORE,
11164
+ DEFAULT_MODEL_ATTRIBUTION: () => DEFAULT_MODEL_ATTRIBUTION,
11165
+ DEFAULT_NEW_PR_LABEL: () => DEFAULT_NEW_PR_LABEL,
10476
11166
  DEFAULT_NOTIFICATIONS: () => DEFAULT_NOTIFICATIONS,
10477
11167
  DEFAULT_PRD_DIR: () => DEFAULT_PRD_DIR,
10478
11168
  DEFAULT_PRD_PRIORITY: () => DEFAULT_PRD_PRIORITY,
@@ -10516,6 +11206,16 @@ __export(dist_exports, {
10516
11206
  DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
10517
11207
  DEFAULT_SUMMARY_WINDOW_HOURS: () => DEFAULT_SUMMARY_WINDOW_HOURS,
10518
11208
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
11209
+ DEFAULT_UX: () => DEFAULT_UX,
11210
+ DEFAULT_UX_AUTO_INSTALL_PLAYWRIGHT: () => DEFAULT_UX_AUTO_INSTALL_PLAYWRIGHT,
11211
+ DEFAULT_UX_BASE_URL: () => DEFAULT_UX_BASE_URL,
11212
+ DEFAULT_UX_ENABLED: () => DEFAULT_UX_ENABLED,
11213
+ DEFAULT_UX_MAX_ISSUES: () => DEFAULT_UX_MAX_ISSUES,
11214
+ DEFAULT_UX_MAX_RUNTIME: () => DEFAULT_UX_MAX_RUNTIME,
11215
+ DEFAULT_UX_REPORT_PROMPT: () => DEFAULT_UX_REPORT_PROMPT,
11216
+ DEFAULT_UX_SCHEDULE: () => DEFAULT_UX_SCHEDULE,
11217
+ DEFAULT_UX_START_URL: () => DEFAULT_UX_START_URL,
11218
+ DEFAULT_UX_TARGET_COLUMN: () => DEFAULT_UX_TARGET_COLUMN,
10519
11219
  DEFAULT_WEBHOOK_TRIGGERS: () => DEFAULT_WEBHOOK_TRIGGERS,
10520
11220
  DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS: () => DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS,
10521
11221
  DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV: () => DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV,
@@ -10556,6 +11256,10 @@ __export(dist_exports, {
10556
11256
  STATE_DB_FILE_NAME: () => STATE_DB_FILE_NAME,
10557
11257
  SqliteKanbanIssueRepository: () => SqliteKanbanIssueRepository,
10558
11258
  SqliteSessionOutcomeRepository: () => SqliteSessionOutcomeRepository,
11259
+ TELEMETRY_ERROR_CATEGORIES: () => TELEMETRY_ERROR_CATEGORIES,
11260
+ TELEMETRY_EVENT_NAMES: () => TELEMETRY_EVENT_NAMES,
11261
+ TELEMETRY_FILE_NAME: () => TELEMETRY_FILE_NAME,
11262
+ UX_LOG_NAME: () => UX_LOG_NAME,
10559
11263
  VALID_CLAUDE_MODELS: () => VALID_CLAUDE_MODELS,
10560
11264
  VALID_JOB_TYPES: () => VALID_JOB_TYPES,
10561
11265
  VALID_MERGE_METHODS: () => VALID_MERGE_METHODS,
@@ -10573,6 +11277,8 @@ __export(dist_exports, {
10573
11277
  buildManagerDraftTitle: () => buildManagerDraftTitle,
10574
11278
  buildProjectFeedbackPromptBlock: () => buildProjectFeedbackPromptBlock,
10575
11279
  buildSessionOutcomeInput: () => buildSessionOutcomeInput,
11280
+ buildUxAuditPrompt: () => buildUxAuditPrompt,
11281
+ buildUxReportBody: () => buildUxReportBody,
10576
11282
  calculateStringSimilarity: () => calculateStringSimilarity,
10577
11283
  camelToUpperSnake: () => camelToUpperSnake,
10578
11284
  canStartJob: () => canStartJob,
@@ -10688,6 +11394,8 @@ __export(dist_exports, {
10688
11394
  getStateFilePath: () => getStateFilePath,
10689
11395
  getStateItem: () => getStateItem,
10690
11396
  getSummaryData: () => getSummaryData,
11397
+ getTelemetryConfigPath: () => getTelemetryConfigPath,
11398
+ getTelemetryEffectiveState: () => getTelemetryEffectiveState,
10691
11399
  getUncheckedItems: () => getUncheckedItems,
10692
11400
  getValidJobTypes: () => getValidJobTypes,
10693
11401
  groupBySection: () => groupBySection2,
@@ -10696,6 +11404,7 @@ __export(dist_exports, {
10696
11404
  header: () => header,
10697
11405
  info: () => info,
10698
11406
  initContainer: () => initContainer,
11407
+ isAllowedTelemetryEventName: () => isAllowedTelemetryEventName,
10699
11408
  isContainerInitialized: () => isContainerInitialized,
10700
11409
  isFeedbackPromptEnabled: () => isFeedbackPromptEnabled,
10701
11410
  isInCooldown: () => isInCooldown,
@@ -10704,6 +11413,7 @@ __export(dist_exports, {
10704
11413
  isKnownFinding: () => isKnownFinding,
10705
11414
  isProcessAliveSince: () => isProcessAliveSince,
10706
11415
  isProcessRunning: () => isProcessRunning,
11416
+ isTelemetryEnvDisabled: () => isTelemetryEnvDisabled,
10707
11417
  isValidCategory: () => isValidCategory,
10708
11418
  isValidHorizon: () => isValidHorizon,
10709
11419
  isValidPriority: () => isValidPriority,
@@ -10715,24 +11425,29 @@ __export(dist_exports, {
10715
11425
  loadGlobalNotificationsConfig: () => loadGlobalNotificationsConfig,
10716
11426
  loadHistory: () => loadHistory,
10717
11427
  loadManagerMemory: () => loadManagerMemory,
11428
+ loadOrCreateTelemetryConfig: () => loadOrCreateTelemetryConfig,
10718
11429
  loadRegistry: () => loadRegistry,
10719
11430
  loadRoadmapState: () => loadRoadmapState,
10720
11431
  loadSlicerTemplate: () => loadSlicerTemplate,
10721
11432
  managerLockPath: () => managerLockPath,
11433
+ mapTelemetryErrorCategory: () => mapTelemetryErrorCategory,
10722
11434
  markItemProcessed: () => markItemProcessed,
10723
11435
  markJobRunning: () => markJobRunning,
10724
11436
  markPrdDone: () => markPrdDone,
11437
+ markTelemetryNoticeShown: () => markTelemetryNoticeShown,
10725
11438
  mergerLockPath: () => mergerLockPath,
10726
11439
  migrateJsonToSqlite: () => migrateJsonToSqlite,
10727
11440
  normalizeAuditSeverity: () => normalizeAuditSeverity,
10728
11441
  normalizeJobConfig: () => normalizeJobConfig,
10729
11442
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
11443
+ normalizeUxPriority: () => normalizeUxPriority,
10730
11444
  parseAuditFindings: () => parseAuditFindings,
10731
11445
  parseConfigValue: () => parseConfigValue,
10732
11446
  parsePrdDependencies: () => parsePrdDependencies,
10733
11447
  parseRoadmap: () => parseRoadmap,
10734
11448
  parseScriptResult: () => parseScriptResult,
10735
11449
  parseTimeToMinutes: () => parseTimeToMinutes,
11450
+ parseUxFindings: () => parseUxFindings,
10736
11451
  performCancel: () => performCancel,
10737
11452
  plannerLockPath: () => plannerLockPath,
10738
11453
  prResolverLockPath: () => prResolverLockPath,
@@ -10770,6 +11485,8 @@ __export(dist_exports, {
10770
11485
  runAnalytics: () => runAnalytics,
10771
11486
  runManager: () => runManager,
10772
11487
  runMigrations: () => runMigrations,
11488
+ runUx: () => runUx,
11489
+ sanitizeTelemetryEvent: () => sanitizeTelemetryEvent,
10773
11490
  saveConfig: () => saveConfig,
10774
11491
  saveGlobalNotificationsConfig: () => saveGlobalNotificationsConfig,
10775
11492
  saveHistory: () => saveHistory,
@@ -10780,19 +11497,23 @@ __export(dist_exports, {
10780
11497
  sendNotifications: () => sendNotifications,
10781
11498
  sendWebhook: () => sendWebhook,
10782
11499
  setConfigValue: () => setConfigValue,
11500
+ setTelemetryEnabled: () => setTelemetryEnabled,
10783
11501
  sleep: () => sleep,
10784
11502
  sliceNextItem: () => sliceNextItem,
10785
11503
  sliceRoadmapItem: () => sliceRoadmapItem,
10786
11504
  slugify: () => slugify,
10787
11505
  sortByPriority: () => sortByPriority,
11506
+ sortUxFindings: () => sortUxFindings,
10788
11507
  step: () => step,
10789
11508
  success: () => success,
10790
11509
  summarizeCreatedDrafts: () => summarizeCreatedDrafts,
10791
11510
  summarizeSkippedFindings: () => summarizeSkippedFindings,
10792
11511
  syncAuditFindingsToBoard: () => syncAuditFindingsToBoard,
11512
+ trackTelemetryEvent: () => trackTelemetryEvent,
10793
11513
  unmarkItemProcessed: () => unmarkItemProcessed,
10794
11514
  unregisterProject: () => unregisterProject,
10795
11515
  updateJobStatus: () => updateJobStatus,
11516
+ uxLockPath: () => uxLockPath,
10796
11517
  validateProvider: () => validateProvider,
10797
11518
  validateRegistry: () => validateRegistry,
10798
11519
  validateWebhook: () => validateWebhook,
@@ -10848,7 +11569,9 @@ var init_dist = __esm({
10848
11569
  init_job_queue();
10849
11570
  init_summary();
10850
11571
  init_analytics();
11572
+ init_telemetry();
10851
11573
  init_audit();
11574
+ init_ux();
10852
11575
  init_manager();
10853
11576
  init_outcome_parser();
10854
11577
  init_pattern_analyzer();
@@ -10862,30 +11585,99 @@ var init_dist = __esm({
10862
11585
  // src/cli.ts
10863
11586
  import "reflect-metadata";
10864
11587
  import { Command as Command3 } from "commander";
10865
- import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
11588
+ import { existsSync as existsSync37, readFileSync as readFileSync24 } from "fs";
10866
11589
  import { fileURLToPath as fileURLToPath6 } from "url";
10867
- import { dirname as dirname13, join as join39 } from "path";
11590
+ import { dirname as dirname14, join as join41 } from "path";
10868
11591
 
10869
11592
  // src/commands/init.ts
10870
11593
  init_dist();
10871
- import fs26 from "fs";
10872
- import path25 from "path";
11594
+ import fs28 from "fs";
11595
+ import path27 from "path";
10873
11596
  import { execSync as execSync3 } from "child_process";
10874
11597
  import { fileURLToPath as fileURLToPath3 } from "url";
10875
- import { dirname as dirname7, join as join21 } from "path";
11598
+ import { dirname as dirname8, join as join23 } from "path";
10876
11599
  import * as readline from "readline";
10877
- var __filename2 = fileURLToPath3(import.meta.url);
10878
- var __dirname2 = dirname7(__filename2);
10879
- function findTemplatesDir(startDir) {
10880
- let d = startDir;
10881
- for (let i = 0; i < 8; i++) {
10882
- const candidate = join21(d, "templates");
10883
- if (fs26.existsSync(candidate) && fs26.statSync(candidate).isDirectory()) {
10884
- return candidate;
10885
- }
10886
- d = dirname7(d);
10887
- }
10888
- return join21(startDir, "templates");
11600
+
11601
+ // src/commands/shared/telemetry.ts
11602
+ init_dist();
11603
+ var cliVersion = "unknown";
11604
+ var telemetryReporter = (eventName, properties) => trackTelemetryEvent(eventName, properties);
11605
+ function setCliTelemetryVersion(version) {
11606
+ cliVersion = version;
11607
+ }
11608
+ function getNodeMajorVersion() {
11609
+ const major = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
11610
+ return Number.isNaN(major) ? 0 : major;
11611
+ }
11612
+ function getRegisteredProjectCount() {
11613
+ try {
11614
+ return validateRegistry().valid.length;
11615
+ } catch {
11616
+ return 0;
11617
+ }
11618
+ }
11619
+ function buildTelemetryBaseProperties(config) {
11620
+ return {
11621
+ cliVersion,
11622
+ platform: process.platform,
11623
+ nodeMajorVersion: getNodeMajorVersion(),
11624
+ boardMode: config ? config.boardProvider?.enabled !== false : void 0,
11625
+ registeredProjectCount: getRegisteredProjectCount()
11626
+ };
11627
+ }
11628
+ function fireTelemetryEvent(eventName, properties = {}) {
11629
+ void telemetryReporter(eventName, properties).catch(() => void 0);
11630
+ }
11631
+ async function trackCommandStarted(command, config) {
11632
+ await telemetryReporter("command_started", {
11633
+ ...buildTelemetryBaseProperties(config),
11634
+ command
11635
+ }).catch(() => void 0);
11636
+ }
11637
+ async function trackCommandCompleted(command, startedAt, exitCode, config, extraProps = {}) {
11638
+ await telemetryReporter("command_completed", {
11639
+ ...buildTelemetryBaseProperties(config),
11640
+ ...extraProps,
11641
+ command,
11642
+ durationMs: Math.max(0, Date.now() - startedAt),
11643
+ exitCode,
11644
+ success: exitCode === 0,
11645
+ failure: exitCode !== 0
11646
+ }).catch(() => void 0);
11647
+ }
11648
+ async function trackJobStarted(jobType, provider, config) {
11649
+ await telemetryReporter("job_started", {
11650
+ ...buildTelemetryBaseProperties(config),
11651
+ jobType,
11652
+ provider
11653
+ }).catch(() => void 0);
11654
+ }
11655
+ async function trackJobCompletedOrFailed(jobType, provider, startedAt, exitCode, config, errorCategory) {
11656
+ await telemetryReporter(exitCode === 0 ? "job_completed" : "job_failed", {
11657
+ ...buildTelemetryBaseProperties(config),
11658
+ jobType,
11659
+ provider,
11660
+ durationMs: Math.max(0, Date.now() - startedAt),
11661
+ exitCode,
11662
+ success: exitCode === 0,
11663
+ failure: exitCode !== 0,
11664
+ errorCategory: exitCode === 0 ? void 0 : mapTelemetryErrorCategory(errorCategory)
11665
+ }).catch(() => void 0);
11666
+ }
11667
+
11668
+ // src/commands/init.ts
11669
+ var __filename2 = fileURLToPath3(import.meta.url);
11670
+ var __dirname2 = dirname8(__filename2);
11671
+ function findTemplatesDir(startDir) {
11672
+ let d = startDir;
11673
+ for (let i = 0; i < 8; i++) {
11674
+ const candidate = join23(d, "templates");
11675
+ if (fs28.existsSync(candidate) && fs28.statSync(candidate).isDirectory()) {
11676
+ return candidate;
11677
+ }
11678
+ d = dirname8(d);
11679
+ }
11680
+ return join23(startDir, "templates");
10889
11681
  }
10890
11682
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
10891
11683
  var NW_SKILLS = [
@@ -10897,12 +11689,12 @@ var NW_SKILLS = [
10897
11689
  "nw-review"
10898
11690
  ];
10899
11691
  function hasPlaywrightDependency(cwd) {
10900
- const packageJsonPath = path25.join(cwd, "package.json");
10901
- if (!fs26.existsSync(packageJsonPath)) {
11692
+ const packageJsonPath = path27.join(cwd, "package.json");
11693
+ if (!fs28.existsSync(packageJsonPath)) {
10902
11694
  return false;
10903
11695
  }
10904
11696
  try {
10905
- const packageJson2 = JSON.parse(fs26.readFileSync(packageJsonPath, "utf-8"));
11697
+ const packageJson2 = JSON.parse(fs28.readFileSync(packageJsonPath, "utf-8"));
10906
11698
  return Boolean(
10907
11699
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
10908
11700
  );
@@ -10914,7 +11706,7 @@ function detectPlaywright(cwd) {
10914
11706
  if (hasPlaywrightDependency(cwd)) {
10915
11707
  return true;
10916
11708
  }
10917
- if (fs26.existsSync(path25.join(cwd, "node_modules", ".bin", "playwright"))) {
11709
+ if (fs28.existsSync(path27.join(cwd, "node_modules", ".bin", "playwright"))) {
10918
11710
  return true;
10919
11711
  }
10920
11712
  try {
@@ -10930,10 +11722,10 @@ function detectPlaywright(cwd) {
10930
11722
  }
10931
11723
  }
10932
11724
  function resolvePlaywrightInstallCommand(cwd) {
10933
- if (fs26.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) {
11725
+ if (fs28.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) {
10934
11726
  return "pnpm add -D @playwright/test";
10935
11727
  }
10936
- if (fs26.existsSync(path25.join(cwd, "yarn.lock"))) {
11728
+ if (fs28.existsSync(path27.join(cwd, "yarn.lock"))) {
10937
11729
  return "yarn add -D @playwright/test";
10938
11730
  }
10939
11731
  return "npm install -D @playwright/test";
@@ -11079,8 +11871,8 @@ function promptProviderSelection(providers) {
11079
11871
  });
11080
11872
  }
11081
11873
  function ensureDir(dirPath) {
11082
- if (!fs26.existsSync(dirPath)) {
11083
- fs26.mkdirSync(dirPath, { recursive: true });
11874
+ if (!fs28.existsSync(dirPath)) {
11875
+ fs28.mkdirSync(dirPath, { recursive: true });
11084
11876
  }
11085
11877
  }
11086
11878
  function buildInitConfig(params) {
@@ -11107,6 +11899,8 @@ function buildInitConfig(params) {
11107
11899
  reviewerRetryDelay: defaults.reviewerRetryDelay,
11108
11900
  provider: params.provider,
11109
11901
  providerLabel: "",
11902
+ modelAttribution: defaults.modelAttribution,
11903
+ newPrLabel: defaults.newPrLabel,
11110
11904
  executorEnabled: defaults.executorEnabled ?? true,
11111
11905
  reviewerEnabled: params.reviewerEnabled,
11112
11906
  providerEnv: { ...defaults.providerEnv },
@@ -11129,6 +11923,10 @@ function buildInitConfig(params) {
11129
11923
  branchPatterns: [...defaults.qa.branchPatterns]
11130
11924
  },
11131
11925
  audit: { ...defaults.audit },
11926
+ ux: {
11927
+ ...defaults.ux,
11928
+ flows: [...defaults.ux.flows]
11929
+ },
11132
11930
  analytics: { ...defaults.analytics },
11133
11931
  manager: { ...defaults.manager },
11134
11932
  feedback: { ...defaults.feedback },
@@ -11155,30 +11953,30 @@ function buildInitConfig(params) {
11155
11953
  }
11156
11954
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
11157
11955
  if (customTemplatesDir !== null) {
11158
- const customPath = join21(customTemplatesDir, templateName);
11159
- if (fs26.existsSync(customPath)) {
11956
+ const customPath = join23(customTemplatesDir, templateName);
11957
+ if (fs28.existsSync(customPath)) {
11160
11958
  return { path: customPath, source: "custom" };
11161
11959
  }
11162
11960
  }
11163
- return { path: join21(bundledTemplatesDir, templateName), source: "bundled" };
11961
+ return { path: join23(bundledTemplatesDir, templateName), source: "bundled" };
11164
11962
  }
11165
11963
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
11166
- if (fs26.existsSync(targetPath) && !force) {
11964
+ if (fs28.existsSync(targetPath) && !force) {
11167
11965
  console.log(` Skipped (exists): ${targetPath}`);
11168
11966
  return { created: false, source: source ?? "bundled" };
11169
11967
  }
11170
- const templatePath = sourcePath ?? join21(TEMPLATES_DIR, templateName);
11968
+ const templatePath = sourcePath ?? join23(TEMPLATES_DIR, templateName);
11171
11969
  const resolvedSource = source ?? "bundled";
11172
- let content = fs26.readFileSync(templatePath, "utf-8");
11970
+ let content = fs28.readFileSync(templatePath, "utf-8");
11173
11971
  for (const [key, value] of Object.entries(replacements)) {
11174
11972
  content = content.replaceAll(key, value);
11175
11973
  }
11176
- fs26.writeFileSync(targetPath, content);
11974
+ fs28.writeFileSync(targetPath, content);
11177
11975
  console.log(` Created: ${targetPath} (${resolvedSource})`);
11178
11976
  return { created: true, source: resolvedSource };
11179
11977
  }
11180
11978
  function addToGitignore(cwd) {
11181
- const gitignorePath = path25.join(cwd, ".gitignore");
11979
+ const gitignorePath = path27.join(cwd, ".gitignore");
11182
11980
  const entries = [
11183
11981
  {
11184
11982
  pattern: "/logs/",
@@ -11192,13 +11990,13 @@ function addToGitignore(cwd) {
11192
11990
  },
11193
11991
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
11194
11992
  ];
11195
- if (!fs26.existsSync(gitignorePath)) {
11993
+ if (!fs28.existsSync(gitignorePath)) {
11196
11994
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
11197
- fs26.writeFileSync(gitignorePath, lines.join("\n"));
11995
+ fs28.writeFileSync(gitignorePath, lines.join("\n"));
11198
11996
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
11199
11997
  return;
11200
11998
  }
11201
- const content = fs26.readFileSync(gitignorePath, "utf-8");
11999
+ const content = fs28.readFileSync(gitignorePath, "utf-8");
11202
12000
  const missing = entries.filter((e) => !e.check(content));
11203
12001
  if (missing.length === 0) {
11204
12002
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -11206,59 +12004,59 @@ function addToGitignore(cwd) {
11206
12004
  }
11207
12005
  const additions = missing.map((e) => e.pattern).join("\n");
11208
12006
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
11209
- fs26.writeFileSync(gitignorePath, newContent);
12007
+ fs28.writeFileSync(gitignorePath, newContent);
11210
12008
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
11211
12009
  }
11212
12010
  function installSkills(cwd, provider, force, templatesDir) {
11213
- const skillsTemplatesDir = path25.join(templatesDir, "skills");
11214
- if (!fs26.existsSync(skillsTemplatesDir)) {
12011
+ const skillsTemplatesDir = path27.join(templatesDir, "skills");
12012
+ if (!fs28.existsSync(skillsTemplatesDir)) {
11215
12013
  return { location: "", installed: 0, skipped: 0, type: "none" };
11216
12014
  }
11217
12015
  const isClaudeProvider = provider === "claude" || provider.startsWith("claude");
11218
12016
  const isCodexProvider = provider === "codex";
11219
- const claudeDir = path25.join(cwd, ".claude");
11220
- if (isClaudeProvider || fs26.existsSync(claudeDir)) {
12017
+ const claudeDir = path27.join(cwd, ".claude");
12018
+ if (isClaudeProvider || fs28.existsSync(claudeDir)) {
11221
12019
  ensureDir(claudeDir);
11222
- const skillsDir = path25.join(claudeDir, "skills");
12020
+ const skillsDir = path27.join(claudeDir, "skills");
11223
12021
  ensureDir(skillsDir);
11224
12022
  let installed = 0;
11225
12023
  let skipped = 0;
11226
12024
  for (const skillName of NW_SKILLS) {
11227
- const templateFile = path25.join(skillsTemplatesDir, `${skillName}.md`);
11228
- if (!fs26.existsSync(templateFile)) continue;
11229
- const skillDir = path25.join(skillsDir, skillName);
12025
+ const templateFile = path27.join(skillsTemplatesDir, `${skillName}.md`);
12026
+ if (!fs28.existsSync(templateFile)) continue;
12027
+ const skillDir = path27.join(skillsDir, skillName);
11230
12028
  ensureDir(skillDir);
11231
- const target = path25.join(skillDir, "SKILL.md");
11232
- if (fs26.existsSync(target) && !force) {
12029
+ const target = path27.join(skillDir, "SKILL.md");
12030
+ if (fs28.existsSync(target) && !force) {
11233
12031
  skipped++;
11234
12032
  continue;
11235
12033
  }
11236
- fs26.copyFileSync(templateFile, target);
12034
+ fs28.copyFileSync(templateFile, target);
11237
12035
  installed++;
11238
12036
  }
11239
12037
  return { location: ".claude/skills/", installed, skipped, type: "claude" };
11240
12038
  }
11241
12039
  if (isCodexProvider) {
11242
- const agentsFile = path25.join(cwd, "AGENTS.md");
11243
- const blockFile = path25.join(skillsTemplatesDir, "_codex-block.md");
11244
- if (!fs26.existsSync(blockFile)) {
12040
+ const agentsFile = path27.join(cwd, "AGENTS.md");
12041
+ const blockFile = path27.join(skillsTemplatesDir, "_codex-block.md");
12042
+ if (!fs28.existsSync(blockFile)) {
11245
12043
  return { location: "", installed: 0, skipped: 0, type: "none" };
11246
12044
  }
11247
- const block = fs26.readFileSync(blockFile, "utf-8");
12045
+ const block = fs28.readFileSync(blockFile, "utf-8");
11248
12046
  const marker = "## Night Watch Skills";
11249
- if (!fs26.existsSync(agentsFile)) {
11250
- fs26.writeFileSync(agentsFile, block);
12047
+ if (!fs28.existsSync(agentsFile)) {
12048
+ fs28.writeFileSync(agentsFile, block);
11251
12049
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
11252
12050
  }
11253
- const existing = fs26.readFileSync(agentsFile, "utf-8");
12051
+ const existing = fs28.readFileSync(agentsFile, "utf-8");
11254
12052
  if (existing.includes(marker)) {
11255
12053
  if (!force) {
11256
12054
  return { location: "AGENTS.md", installed: 0, skipped: NW_SKILLS.length, type: "codex" };
11257
12055
  }
11258
12056
  const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, "");
11259
- fs26.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
12057
+ fs28.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
11260
12058
  } else {
11261
- fs26.appendFileSync(agentsFile, "\n\n" + block);
12059
+ fs28.appendFileSync(agentsFile, "\n\n" + block);
11262
12060
  }
11263
12061
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
11264
12062
  }
@@ -11382,28 +12180,28 @@ function initCommand(program2) {
11382
12180
  "${DEFAULT_BRANCH}": defaultBranch
11383
12181
  };
11384
12182
  step(6, totalSteps, "Creating PRD directory structure...");
11385
- const prdDirPath = path25.join(cwd, prdDir);
11386
- const doneDirPath = path25.join(prdDirPath, "done");
12183
+ const prdDirPath = path27.join(cwd, prdDir);
12184
+ const doneDirPath = path27.join(prdDirPath, "done");
11387
12185
  ensureDir(doneDirPath);
11388
12186
  success(`Created ${prdDirPath}/`);
11389
12187
  success(`Created ${doneDirPath}/`);
11390
12188
  step(7, totalSteps, "Creating logs directory...");
11391
- const logsPath = path25.join(cwd, LOG_DIR);
12189
+ const logsPath = path27.join(cwd, LOG_DIR);
11392
12190
  ensureDir(logsPath);
11393
12191
  success(`Created ${logsPath}/`);
11394
12192
  addToGitignore(cwd);
11395
12193
  step(8, totalSteps, "Creating instructions directory...");
11396
- const instructionsDir = path25.join(cwd, "instructions");
12194
+ const instructionsDir = path27.join(cwd, "instructions");
11397
12195
  ensureDir(instructionsDir);
11398
12196
  success(`Created ${instructionsDir}/`);
11399
12197
  const existingConfig = loadConfig(cwd);
11400
- const customTemplatesDirPath = path25.join(cwd, existingConfig.templatesDir);
11401
- const customTemplatesDir = fs26.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
12198
+ const customTemplatesDirPath = path27.join(cwd, existingConfig.templatesDir);
12199
+ const customTemplatesDir = fs28.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
11402
12200
  const templateSources = [];
11403
12201
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
11404
12202
  const nwResult = processTemplate(
11405
12203
  "executor.md",
11406
- path25.join(instructionsDir, "executor.md"),
12204
+ path27.join(instructionsDir, "executor.md"),
11407
12205
  replacements,
11408
12206
  force,
11409
12207
  nwResolution.path,
@@ -11417,7 +12215,7 @@ function initCommand(program2) {
11417
12215
  );
11418
12216
  const peResult = processTemplate(
11419
12217
  "prd-executor.md",
11420
- path25.join(instructionsDir, "prd-executor.md"),
12218
+ path27.join(instructionsDir, "prd-executor.md"),
11421
12219
  replacements,
11422
12220
  force,
11423
12221
  peResolution.path,
@@ -11427,7 +12225,7 @@ function initCommand(program2) {
11427
12225
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
11428
12226
  const prResult = processTemplate(
11429
12227
  "pr-reviewer.md",
11430
- path25.join(instructionsDir, "pr-reviewer.md"),
12228
+ path27.join(instructionsDir, "pr-reviewer.md"),
11431
12229
  replacements,
11432
12230
  force,
11433
12231
  prResolution.path,
@@ -11437,7 +12235,7 @@ function initCommand(program2) {
11437
12235
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
11438
12236
  const qaResult = processTemplate(
11439
12237
  "qa.md",
11440
- path25.join(instructionsDir, "qa.md"),
12238
+ path27.join(instructionsDir, "qa.md"),
11441
12239
  replacements,
11442
12240
  force,
11443
12241
  qaResolution.path,
@@ -11447,7 +12245,7 @@ function initCommand(program2) {
11447
12245
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
11448
12246
  const auditResult = processTemplate(
11449
12247
  "audit.md",
11450
- path25.join(instructionsDir, "audit.md"),
12248
+ path27.join(instructionsDir, "audit.md"),
11451
12249
  replacements,
11452
12250
  force,
11453
12251
  auditResolution.path,
@@ -11461,7 +12259,7 @@ function initCommand(program2) {
11461
12259
  );
11462
12260
  const plannerResult = processTemplate(
11463
12261
  "prd-creator.md",
11464
- path25.join(instructionsDir, "prd-creator.md"),
12262
+ path27.join(instructionsDir, "prd-creator.md"),
11465
12263
  replacements,
11466
12264
  force,
11467
12265
  plannerResolution.path,
@@ -11469,8 +12267,8 @@ function initCommand(program2) {
11469
12267
  );
11470
12268
  templateSources.push({ name: "prd-creator.md", source: plannerResult.source });
11471
12269
  step(9, totalSteps, "Creating configuration file...");
11472
- const configPath = path25.join(cwd, CONFIG_FILE_NAME);
11473
- if (fs26.existsSync(configPath) && !force) {
12270
+ const configPath = path27.join(cwd, CONFIG_FILE_NAME);
12271
+ if (fs28.existsSync(configPath) && !force) {
11474
12272
  console.log(` Skipped (exists): ${configPath}`);
11475
12273
  } else {
11476
12274
  const config = buildInitConfig({
@@ -11480,11 +12278,11 @@ function initCommand(program2) {
11480
12278
  reviewerEnabled,
11481
12279
  prdDir
11482
12280
  });
11483
- fs26.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
12281
+ fs28.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
11484
12282
  success(`Created ${configPath}`);
11485
12283
  }
11486
12284
  step(10, totalSteps, "Setting up GitHub Project board...");
11487
- const existingRaw = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
12285
+ const existingRaw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
11488
12286
  const existingBoard = existingRaw.boardProvider;
11489
12287
  let boardSetupStatus = "Skipped";
11490
12288
  if (existingBoard?.projectNumber && !force) {
@@ -11506,14 +12304,14 @@ function initCommand(program2) {
11506
12304
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
11507
12305
  const boardTitle = `${projectName} Night Watch`;
11508
12306
  const board = await provider.setupBoard(boardTitle);
11509
- const rawConfig = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
12307
+ const rawConfig = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
11510
12308
  rawConfig.boardProvider = {
11511
12309
  enabled: true,
11512
12310
  provider: "github",
11513
12311
  projectNumber: board.number,
11514
12312
  projectTitle: board.title
11515
12313
  };
11516
- fs26.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
12314
+ fs28.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
11517
12315
  boardSetupStatus = `Created (#${board.number})`;
11518
12316
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
11519
12317
  } catch (boardErr) {
@@ -11605,6 +12403,12 @@ function initCommand(program2) {
11605
12403
  label("Reviewer", reviewerEnabled ? "Enabled" : "Disabled");
11606
12404
  label("Playwright", playwrightStatus);
11607
12405
  console.log();
12406
+ fireTelemetryEvent("cli_init_completed", {
12407
+ command: "init",
12408
+ provider: selectedProvider,
12409
+ boardMode: existingRaw.boardProvider?.enabled !== false,
12410
+ success: true
12411
+ });
11608
12412
  header("Next Steps");
11609
12413
  info(`1. Add your PRD files to ${prdDir}/`);
11610
12414
  info("2. Run `night-watch install` to set up cron jobs");
@@ -11739,8 +12543,8 @@ function recordJobOutcome(input) {
11739
12543
  }
11740
12544
 
11741
12545
  // src/commands/run.ts
11742
- import * as fs27 from "fs";
11743
- import * as path26 from "path";
12546
+ import * as fs29 from "fs";
12547
+ import * as path28 from "path";
11744
12548
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
11745
12549
  if (exitCode === 124) {
11746
12550
  return "run_timeout";
@@ -11838,7 +12642,7 @@ function buildRunNotificationContext(config, projectDir, event, exitCode, script
11838
12642
  const checkpointStatus = checkpointValue === "created" || checkpointValue === "available" || checkpointValue === "none" ? checkpointValue : void 0;
11839
12643
  return {
11840
12644
  event,
11841
- projectName: path26.basename(projectDir),
12645
+ projectName: path28.basename(projectDir),
11842
12646
  exitCode,
11843
12647
  provider: config.provider,
11844
12648
  prdName: scriptResult?.data.prd ?? extractResultValueFromOutput(rawOutput, "prd"),
@@ -11858,12 +12662,12 @@ function buildRunNotificationContext(config, projectDir, event, exitCode, script
11858
12662
  };
11859
12663
  }
11860
12664
  function getCrossProjectFallbackCandidates(currentProjectDir) {
11861
- const current = path26.resolve(currentProjectDir);
12665
+ const current = path28.resolve(currentProjectDir);
11862
12666
  const { valid, invalid } = validateRegistry();
11863
12667
  for (const entry of invalid) {
11864
12668
  warn(`Skipping invalid registry entry: ${entry.path}`);
11865
12669
  }
11866
- return valid.filter((entry) => path26.resolve(entry.path) !== current);
12670
+ return valid.filter((entry) => path28.resolve(entry.path) !== current);
11867
12671
  }
11868
12672
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult, rawOutput) {
11869
12673
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -11873,7 +12677,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
11873
12677
  if (nonTelegramWebhooks.length > 0) {
11874
12678
  const _rateLimitCtx = {
11875
12679
  event: "rate_limit_fallback",
11876
- projectName: path26.basename(projectDir),
12680
+ projectName: path28.basename(projectDir),
11877
12681
  exitCode,
11878
12682
  provider: config.provider
11879
12683
  };
@@ -12033,6 +12837,8 @@ function buildEnvVars(config, options) {
12033
12837
  }
12034
12838
  env.NW_PRD_DIR = config.prdDir;
12035
12839
  env.NW_BRANCH_PREFIX = config.branchPrefix;
12840
+ env.NW_MODEL_ATTRIBUTION_ENABLED = config.modelAttribution ? "1" : "0";
12841
+ env.NW_NEW_PR_LABEL = config.newPrLabel ?? "draft";
12036
12842
  if (config.prdPriority && config.prdPriority.length > 0) {
12037
12843
  env.NW_PRD_PRIORITY = config.prdPriority.join(":");
12038
12844
  }
@@ -12111,20 +12917,20 @@ function applyCliOverrides(config, options) {
12111
12917
  }
12112
12918
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
12113
12919
  const claimStaleAfter = maxRuntime > 0 ? maxRuntime : 14400;
12114
- const absolutePrdDir = path26.join(projectDir, prdDir);
12115
- const doneDir = path26.join(absolutePrdDir, "done");
12920
+ const absolutePrdDir = path28.join(projectDir, prdDir);
12921
+ const doneDir = path28.join(absolutePrdDir, "done");
12116
12922
  const pending = [];
12117
12923
  const completed = [];
12118
- if (fs27.existsSync(absolutePrdDir)) {
12119
- const entries = fs27.readdirSync(absolutePrdDir, { withFileTypes: true });
12924
+ if (fs29.existsSync(absolutePrdDir)) {
12925
+ const entries = fs29.readdirSync(absolutePrdDir, { withFileTypes: true });
12120
12926
  for (const entry of entries) {
12121
12927
  if (entry.isFile() && entry.name.endsWith(".md")) {
12122
- const claimPath = path26.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
12928
+ const claimPath = path28.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
12123
12929
  let claimed = false;
12124
12930
  let claimInfo = null;
12125
- if (fs27.existsSync(claimPath)) {
12931
+ if (fs29.existsSync(claimPath)) {
12126
12932
  try {
12127
- const content = fs27.readFileSync(claimPath, "utf-8");
12933
+ const content = fs29.readFileSync(claimPath, "utf-8");
12128
12934
  const data = JSON.parse(content);
12129
12935
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
12130
12936
  if (age < claimStaleAfter) {
@@ -12138,8 +12944,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
12138
12944
  }
12139
12945
  }
12140
12946
  }
12141
- if (fs27.existsSync(doneDir)) {
12142
- const entries = fs27.readdirSync(doneDir, { withFileTypes: true });
12947
+ if (fs29.existsSync(doneDir)) {
12948
+ const entries = fs29.readdirSync(doneDir, { withFileTypes: true });
12143
12949
  for (const entry of entries) {
12144
12950
  if (entry.isFile() && entry.name.endsWith(".md")) {
12145
12951
  completed.push(entry.name);
@@ -12159,8 +12965,11 @@ function runCommand(program2) {
12159
12965
  const projectDir = process.cwd();
12160
12966
  let config = loadConfig(projectDir);
12161
12967
  config = applyCliOverrides(config, options);
12968
+ const commandStartedAt = Date.now();
12969
+ await trackCommandStarted("run", config);
12162
12970
  if (config.executorEnabled === false && !options.dryRun) {
12163
12971
  info("Executor is disabled in config; skipping run.");
12972
+ await trackCommandCompleted("run", commandStartedAt, 0, config);
12164
12973
  process.exit(0);
12165
12974
  }
12166
12975
  const envVars = buildEnvVars(config, options);
@@ -12252,12 +13061,15 @@ function runCommand(program2) {
12252
13061
  header("Command");
12253
13062
  dim(` bash ${scriptPath} ${projectDir}`);
12254
13063
  console.log();
13064
+ await trackCommandCompleted("run", commandStartedAt, 0, config);
12255
13065
  process.exit(0);
12256
13066
  }
12257
13067
  const spinner = createSpinner("Running PRD executor...");
12258
13068
  spinner.start();
12259
13069
  try {
12260
13070
  const startedAt = Date.now();
13071
+ const provider = envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "executor");
13072
+ await trackJobStarted("executor", provider, config);
12261
13073
  await maybeApplyCronSchedulingDelay(config, "executor", projectDir);
12262
13074
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
12263
13075
  scriptPath,
@@ -12268,6 +13080,14 @@ function runCommand(program2) {
12268
13080
  const finishedAt = Date.now();
12269
13081
  const scriptResult = parseScriptResult(`${stdout}
12270
13082
  ${stderr}`);
13083
+ await trackJobCompletedOrFailed(
13084
+ "executor",
13085
+ provider,
13086
+ startedAt,
13087
+ exitCode,
13088
+ config,
13089
+ scriptResult?.status
13090
+ );
12271
13091
  if (exitCode === 0) {
12272
13092
  if (scriptResult?.status === "queued") {
12273
13093
  spinner.succeed("PRD executor queued \u2014 another job is currently running");
@@ -12306,16 +13126,29 @@ ${stderr}`);
12306
13126
  ${stderr}`
12307
13127
  );
12308
13128
  }
13129
+ if (exitCode === 0 && (scriptResult?.status === "success_open_pr" || getRunPrMetadata(scriptResult, `${stdout}
13130
+ ${stderr}`).prUrl)) {
13131
+ fireTelemetryEvent("pr_opened", {
13132
+ ...buildTelemetryBaseProperties(config),
13133
+ jobType: "executor",
13134
+ provider,
13135
+ success: true
13136
+ });
13137
+ }
12309
13138
  if (shouldAttemptCrossProjectFallback(options, scriptResult?.status)) {
12310
13139
  const executedFallback = await runCrossProjectFallback(projectDir, options);
12311
13140
  if (!executedFallback) {
12312
13141
  info("Cross-project fallback: no eligible work found in other registered projects");
12313
13142
  }
12314
13143
  }
13144
+ await trackCommandCompleted("run", commandStartedAt, exitCode, config);
12315
13145
  process.exit(exitCode);
12316
13146
  } catch (err) {
12317
13147
  spinner.fail("Failed to execute run command");
12318
13148
  error(`${err instanceof Error ? err.message : String(err)}`);
13149
+ await trackCommandCompleted("run", commandStartedAt, 1, config, {
13150
+ errorCategory: err instanceof Error ? err.message : String(err)
13151
+ });
12319
13152
  process.exit(1);
12320
13153
  }
12321
13154
  });
@@ -12324,7 +13157,7 @@ ${stderr}`
12324
13157
  // src/commands/review.ts
12325
13158
  init_dist();
12326
13159
  import { execFileSync as execFileSync5 } from "child_process";
12327
- import * as path27 from "path";
13160
+ import * as path29 from "path";
12328
13161
  function shouldSendReviewNotification(scriptStatus) {
12329
13162
  if (!scriptStatus) {
12330
13163
  return true;
@@ -12497,8 +13330,11 @@ function reviewCommand(program2) {
12497
13330
  const projectDir = process.cwd();
12498
13331
  let config = loadConfig(projectDir);
12499
13332
  config = applyCliOverrides2(config, options);
13333
+ const commandStartedAt = Date.now();
13334
+ await trackCommandStarted("review", config);
12500
13335
  if (!config.reviewerEnabled && !options.dryRun) {
12501
13336
  info("Reviewer is disabled in config; skipping review.");
13337
+ await trackCommandCompleted("review", commandStartedAt, 0, config);
12502
13338
  process.exit(0);
12503
13339
  }
12504
13340
  const envVars = buildEnvVars2(config, options);
@@ -12547,6 +13383,7 @@ function reviewCommand(program2) {
12547
13383
  header("Command");
12548
13384
  dim(` bash ${scriptPath} ${projectDir}`);
12549
13385
  console.log();
13386
+ await trackCommandCompleted("review", commandStartedAt, 0, config);
12550
13387
  process.exit(0);
12551
13388
  }
12552
13389
  const preflightOpenPrs = getOpenPrsNeedingWork(config.branchPatterns);
@@ -12568,6 +13405,8 @@ function reviewCommand(program2) {
12568
13405
  spinner.start();
12569
13406
  try {
12570
13407
  const startedAt = Date.now();
13408
+ const provider = envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "reviewer");
13409
+ await trackJobStarted("reviewer", provider, config);
12571
13410
  await maybeApplyCronSchedulingDelay(config, "reviewer", projectDir);
12572
13411
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
12573
13412
  scriptPath,
@@ -12577,6 +13416,14 @@ function reviewCommand(program2) {
12577
13416
  const finishedAt = Date.now();
12578
13417
  const scriptResult = parseScriptResult(`${stdout}
12579
13418
  ${stderr}`);
13419
+ await trackJobCompletedOrFailed(
13420
+ "reviewer",
13421
+ provider,
13422
+ startedAt,
13423
+ exitCode,
13424
+ config,
13425
+ scriptResult?.status
13426
+ );
12580
13427
  if (exitCode === 0) {
12581
13428
  if (scriptResult?.status === "queued") {
12582
13429
  spinner.succeed("PR reviewer queued \u2014 another job is currently running");
@@ -12620,6 +13467,13 @@ ${stderr}`);
12620
13467
  );
12621
13468
  if (!shouldNotifyCompletion) {
12622
13469
  info("Skipping review completion notification (review did not complete successfully)");
13470
+ } else {
13471
+ fireTelemetryEvent("review_completed", {
13472
+ ...buildTelemetryBaseProperties(config),
13473
+ jobType: "reviewer",
13474
+ provider,
13475
+ success: true
13476
+ });
12623
13477
  }
12624
13478
  let fallbackPrDetails = null;
12625
13479
  if (shouldNotifyCompletion) {
@@ -12652,7 +13506,7 @@ ${stderr}`);
12652
13506
  const reviewEvent = legacyNoChangesNeeded ? "review_ready_for_human" : "review_completed";
12653
13507
  await sendNotifications(config, {
12654
13508
  event: reviewEvent,
12655
- projectName: path27.basename(projectDir),
13509
+ projectName: path29.basename(projectDir),
12656
13510
  exitCode,
12657
13511
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
12658
13512
  prUrl: fallbackPrDetails?.url,
@@ -12671,7 +13525,7 @@ ${stderr}`);
12671
13525
  const reviewEvent = target.noChangesNeeded ? "review_ready_for_human" : "review_completed";
12672
13526
  await sendNotifications(config, {
12673
13527
  event: reviewEvent,
12674
- projectName: path27.basename(projectDir),
13528
+ projectName: path29.basename(projectDir),
12675
13529
  exitCode,
12676
13530
  provider: formatProviderDisplay(
12677
13531
  envVars.NW_PROVIDER_CMD,
@@ -12692,11 +13546,17 @@ ${stderr}`);
12692
13546
  }
12693
13547
  const autoMergedPrNumbers = parseAutoMergedPrNumbers(scriptResult?.data.auto_merged);
12694
13548
  if (autoMergedPrNumbers.length > 0) {
13549
+ fireTelemetryEvent("auto_merge_completed", {
13550
+ ...buildTelemetryBaseProperties(config),
13551
+ jobType: "reviewer",
13552
+ provider,
13553
+ success: true
13554
+ });
12695
13555
  const autoMergedPrNumber = autoMergedPrNumbers[0];
12696
13556
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
12697
13557
  const _mergeCtx = {
12698
13558
  event: "pr_auto_merged",
12699
- projectName: path27.basename(projectDir),
13559
+ projectName: path29.basename(projectDir),
12700
13560
  exitCode,
12701
13561
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
12702
13562
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -12710,10 +13570,14 @@ ${stderr}`);
12710
13570
  await sendNotifications(config, _mergeCtx);
12711
13571
  }
12712
13572
  }
13573
+ await trackCommandCompleted("review", commandStartedAt, exitCode, config);
12713
13574
  process.exit(exitCode);
12714
13575
  } catch (err) {
12715
13576
  spinner.fail("Failed to execute review command");
12716
13577
  error(`${err instanceof Error ? err.message : String(err)}`);
13578
+ await trackCommandCompleted("review", commandStartedAt, 1, config, {
13579
+ errorCategory: err instanceof Error ? err.message : String(err)
13580
+ });
12717
13581
  process.exit(1);
12718
13582
  }
12719
13583
  });
@@ -12721,7 +13585,7 @@ ${stderr}`);
12721
13585
 
12722
13586
  // src/commands/qa.ts
12723
13587
  init_dist();
12724
- import * as path28 from "path";
13588
+ import * as path30 from "path";
12725
13589
  function shouldSendQaNotification(scriptStatus) {
12726
13590
  if (!scriptStatus) {
12727
13591
  return true;
@@ -12883,7 +13747,7 @@ ${stderr}`);
12883
13747
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
12884
13748
  const _qaCtx = {
12885
13749
  event: "qa_completed",
12886
- projectName: path28.basename(projectDir),
13750
+ projectName: path30.basename(projectDir),
12887
13751
  exitCode,
12888
13752
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
12889
13753
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -12909,8 +13773,8 @@ ${stderr}`);
12909
13773
 
12910
13774
  // src/commands/audit.ts
12911
13775
  init_dist();
12912
- import * as fs28 from "fs";
12913
- import * as path29 from "path";
13776
+ import * as fs30 from "fs";
13777
+ import * as path31 from "path";
12914
13778
  function buildEnvVars4(config, options) {
12915
13779
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
12916
13780
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -12958,7 +13822,7 @@ function auditCommand(program2) {
12958
13822
  if (config.audit.createIssues) {
12959
13823
  configTable.push(["Target Column", config.audit.targetColumn]);
12960
13824
  }
12961
- configTable.push(["Report File", path29.join(projectDir, "logs", "audit-report.md")]);
13825
+ configTable.push(["Report File", path31.join(projectDir, "logs", "audit-report.md")]);
12962
13826
  console.log(configTable.toString());
12963
13827
  header("Provider Invocation");
12964
13828
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -13016,8 +13880,8 @@ ${stderr}`);
13016
13880
  } else if (scriptResult?.status?.startsWith("skip_")) {
13017
13881
  spinner.succeed("Code audit skipped");
13018
13882
  } else {
13019
- const reportPath = path29.join(projectDir, "logs", "audit-report.md");
13020
- if (!fs28.existsSync(reportPath)) {
13883
+ const reportPath = path31.join(projectDir, "logs", "audit-report.md");
13884
+ if (!fs30.existsSync(reportPath)) {
13021
13885
  spinner.fail("Code audit finished without a report file");
13022
13886
  process.exit(1);
13023
13887
  }
@@ -13034,9 +13898,9 @@ ${stderr}`);
13034
13898
  const providerExit = scriptResult?.data?.provider_exit;
13035
13899
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
13036
13900
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
13037
- const logPath = path29.join(projectDir, "logs", "audit.log");
13038
- if (fs28.existsSync(logPath)) {
13039
- const logLines = fs28.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
13901
+ const logPath = path31.join(projectDir, "logs", "audit.log");
13902
+ if (fs30.existsSync(logPath)) {
13903
+ const logLines = fs30.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
13040
13904
  if (logLines.length > 0) {
13041
13905
  process.stderr.write(logLines.join("\n") + "\n");
13042
13906
  }
@@ -13067,6 +13931,153 @@ ${stderr}`);
13067
13931
  });
13068
13932
  }
13069
13933
 
13934
+ // src/commands/ux.ts
13935
+ init_dist();
13936
+ function parseTimeout(timeout) {
13937
+ if (!timeout) return void 0;
13938
+ const parsed = parseInt(timeout, 10);
13939
+ return Number.isNaN(parsed) || parsed < 0 ? void 0 : parsed;
13940
+ }
13941
+ function applyUxCliOverrides(config, options) {
13942
+ let overridden = config;
13943
+ const timeout = parseTimeout(options.timeout);
13944
+ if (timeout !== void 0) {
13945
+ overridden = { ...overridden, ux: { ...overridden.ux, maxRuntime: timeout } };
13946
+ }
13947
+ if (options.provider) {
13948
+ overridden = {
13949
+ ...overridden,
13950
+ _cliProviderOverride: options.provider
13951
+ };
13952
+ }
13953
+ return overridden;
13954
+ }
13955
+ function writeJson(value) {
13956
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
13957
+ `);
13958
+ }
13959
+ function cleanupQueueEntry() {
13960
+ const queueId = process.env.NW_QUEUE_ENTRY_ID ? parseInt(process.env.NW_QUEUE_ENTRY_ID, 10) : NaN;
13961
+ if (Number.isNaN(queueId) || queueId < 1) return;
13962
+ removeJob(queueId);
13963
+ }
13964
+ function printDryRun(config) {
13965
+ header("Dry Run: UX Agent");
13966
+ const table = createTable({ head: ["Setting", "Value"] });
13967
+ table.push(["Provider", resolveJobProvider(config, "ux")]);
13968
+ table.push(["Enabled", config.ux.enabled ? "yes" : "no"]);
13969
+ table.push(["Max Runtime", `${config.ux.maxRuntime}s`]);
13970
+ table.push(["Target Column", config.ux.targetColumn]);
13971
+ table.push(["Base URL", config.ux.baseUrl || "(not configured)"]);
13972
+ table.push(["Start URL", config.ux.startUrl || "(not configured)"]);
13973
+ table.push(["Flows", config.ux.flows.length > 0 ? config.ux.flows.join(", ") : "(discover)"]);
13974
+ table.push(["Auto-install Playwright", config.ux.autoInstallPlaywright ? "yes" : "no"]);
13975
+ table.push(["Max Issues", String(config.ux.maxIssues)]);
13976
+ console.log(table.toString());
13977
+ }
13978
+ function uxCommand(program2) {
13979
+ program2.command("ux").description("Run UX agent to inspect user flows and draft a prioritized report").option("--dry-run", "Show what would be executed without running").option("--json", "Output structured JSON").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use").action(async (options) => {
13980
+ const projectDir = process.cwd();
13981
+ let config = loadConfig(projectDir);
13982
+ config = applyUxCliOverrides(config, options);
13983
+ if (!config.ux.enabled && !options.dryRun) {
13984
+ cleanupQueueEntry();
13985
+ if (options.json) {
13986
+ writeJson({ skipped: true, reason: "ux-disabled" });
13987
+ } else {
13988
+ info("UX agent is disabled in config; skipping run.");
13989
+ }
13990
+ process.exit(0);
13991
+ }
13992
+ if (options.dryRun) {
13993
+ if (options.json) {
13994
+ writeJson({
13995
+ dryRun: true,
13996
+ provider: resolveJobProvider(config, "ux"),
13997
+ config: config.ux
13998
+ });
13999
+ } else {
14000
+ printDryRun(config);
14001
+ }
14002
+ process.exit(0);
14003
+ }
14004
+ const spinner = options.json ? null : createSpinner("Running UX agent...");
14005
+ spinner?.start();
14006
+ const startedAt = Date.now();
14007
+ let exitCode = 0;
14008
+ const lockPath = uxLockPath(projectDir);
14009
+ let lockAcquired = false;
14010
+ try {
14011
+ await maybeApplyCronSchedulingDelay(config, "ux", projectDir);
14012
+ lockAcquired = acquireLock(lockPath);
14013
+ if (!lockAcquired) {
14014
+ cleanupQueueEntry();
14015
+ if (options.json) {
14016
+ writeJson({ skipped: true, reason: "ux-locked" });
14017
+ } else {
14018
+ spinner?.succeed("UX agent skipped: already running.");
14019
+ }
14020
+ process.exit(0);
14021
+ }
14022
+ const result = await runUx(config, projectDir);
14023
+ exitCode = result.issuesCreated >= 0 ? 0 : 1;
14024
+ try {
14025
+ recordJobOutcome({
14026
+ config,
14027
+ exitCode,
14028
+ finishedAt: Date.now(),
14029
+ jobType: "ux",
14030
+ metadata: {
14031
+ findings: result.findings.length,
14032
+ issuesCreated: result.issuesCreated,
14033
+ reportUrl: result.reportUrl,
14034
+ summary: result.summary
14035
+ },
14036
+ projectDir,
14037
+ providerKey: resolveJobProvider(config, "ux"),
14038
+ startedAt,
14039
+ stdout: result.summary
14040
+ });
14041
+ } catch {
14042
+ }
14043
+ if (options.json) {
14044
+ writeJson(result);
14045
+ } else {
14046
+ spinner?.succeed(`UX agent complete \u2014 ${result.summary}`);
14047
+ }
14048
+ } catch (err) {
14049
+ const message = err instanceof Error ? err.message : String(err);
14050
+ try {
14051
+ recordJobOutcome({
14052
+ config,
14053
+ exitCode: 1,
14054
+ finishedAt: Date.now(),
14055
+ jobType: "ux",
14056
+ metadata: { error: message },
14057
+ projectDir,
14058
+ providerKey: resolveJobProvider(config, "ux"),
14059
+ startedAt,
14060
+ stderr: message
14061
+ });
14062
+ } catch {
14063
+ }
14064
+ if (options.json) {
14065
+ process.stderr.write(`${JSON.stringify({ ok: false, error: message }, null, 2)}
14066
+ `);
14067
+ } else {
14068
+ spinner?.fail(`UX agent failed: ${message}`);
14069
+ }
14070
+ process.exit(1);
14071
+ } finally {
14072
+ if (lockAcquired) {
14073
+ releaseLock(lockPath);
14074
+ }
14075
+ cleanupQueueEntry();
14076
+ }
14077
+ process.exit(exitCode);
14078
+ });
14079
+ }
14080
+
13070
14081
  // src/commands/analytics.ts
13071
14082
  init_dist();
13072
14083
  function analyticsCommand(program2) {
@@ -13180,16 +14191,16 @@ function analyticsCommand(program2) {
13180
14191
  // src/commands/install.ts
13181
14192
  init_dist();
13182
14193
  import { execSync as execSync4 } from "child_process";
13183
- import * as path30 from "path";
13184
- import * as fs29 from "fs";
13185
- function shellQuote(value) {
14194
+ import * as path32 from "path";
14195
+ import * as fs31 from "fs";
14196
+ function shellQuote2(value) {
13186
14197
  return `'${value.replace(/'/g, `'"'"'`)}'`;
13187
14198
  }
13188
14199
  function getNightWatchBinPath() {
13189
14200
  try {
13190
14201
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
13191
- const binPath = path30.join(npmBin, "night-watch");
13192
- if (fs29.existsSync(binPath)) {
14202
+ const binPath = path32.join(npmBin, "night-watch");
14203
+ if (fs31.existsSync(binPath)) {
13193
14204
  return binPath;
13194
14205
  }
13195
14206
  } catch {
@@ -13202,17 +14213,17 @@ function getNightWatchBinPath() {
13202
14213
  }
13203
14214
  function getNodeBinDir() {
13204
14215
  if (process.execPath && process.execPath !== "node") {
13205
- return path30.dirname(process.execPath);
14216
+ return path32.dirname(process.execPath);
13206
14217
  }
13207
14218
  try {
13208
14219
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
13209
- return path30.dirname(nodePath);
14220
+ return path32.dirname(nodePath);
13210
14221
  } catch {
13211
14222
  return "";
13212
14223
  }
13213
14224
  }
13214
14225
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
13215
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path30.dirname(nightWatchBin) : "";
14226
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path32.dirname(nightWatchBin) : "";
13216
14227
  const pathParts = Array.from(
13217
14228
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
13218
14229
  );
@@ -13228,12 +14239,12 @@ function performInstall(projectDir, config, options) {
13228
14239
  const nightWatchBin = getNightWatchBinPath();
13229
14240
  const projectName = getProjectName(projectDir);
13230
14241
  const marker = generateMarker(projectName);
13231
- const logDir = path30.join(projectDir, LOG_DIR);
13232
- if (!fs29.existsSync(logDir)) {
13233
- fs29.mkdirSync(logDir, { recursive: true });
14242
+ const logDir = path32.join(projectDir, LOG_DIR);
14243
+ if (!fs31.existsSync(logDir)) {
14244
+ fs31.mkdirSync(logDir, { recursive: true });
13234
14245
  }
13235
- const executorLog = path30.join(logDir, "executor.log");
13236
- const reviewerLog = path30.join(logDir, "reviewer.log");
14246
+ const executorLog = path32.join(logDir, "executor.log");
14247
+ const reviewerLog = path32.join(logDir, "reviewer.log");
13237
14248
  if (!options?.force) {
13238
14249
  const existingEntries2 = Array.from(
13239
14250
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -13250,76 +14261,84 @@ function performInstall(projectDir, config, options) {
13250
14261
  const entries = [];
13251
14262
  const nodeBinDir = getNodeBinDir();
13252
14263
  const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
13253
- const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
14264
+ const cliBinPrefix = `export NW_CLI_BIN=${shellQuote2(nightWatchBin)} && `;
13254
14265
  const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
13255
14266
  let providerEnvPrefix = "";
13256
14267
  if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
13257
- const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
14268
+ const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote2(value)}`).join(" && ");
13258
14269
  providerEnvPrefix = exports + " && ";
13259
14270
  }
13260
14271
  const installExecutor = config.executorEnabled !== false;
13261
14272
  if (installExecutor) {
13262
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
14273
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} run >> ${shellQuote2(executorLog)} 2>&1 ${marker}`;
13263
14274
  entries.push(executorEntry);
13264
14275
  }
13265
14276
  const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
13266
14277
  if (installReviewer) {
13267
- const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
14278
+ const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} review >> ${shellQuote2(reviewerLog)} 2>&1 ${marker}`;
13268
14279
  entries.push(reviewerEntry);
13269
14280
  }
13270
14281
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
13271
14282
  if (installSlicer) {
13272
14283
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
13273
- const slicerLog = path30.join(logDir, "slicer.log");
13274
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
14284
+ const slicerLog = path32.join(logDir, "slicer.log");
14285
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} planner >> ${shellQuote2(slicerLog)} 2>&1 ${marker}`;
13275
14286
  entries.push(slicerEntry);
13276
14287
  }
13277
14288
  const disableQa = options?.noQa === true || options?.qa === false;
13278
14289
  const installQa = disableQa ? false : config.qa.enabled;
13279
14290
  if (installQa) {
13280
14291
  const qaSchedule = config.qa.schedule;
13281
- const qaLog = path30.join(logDir, "qa.log");
13282
- const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
14292
+ const qaLog = path32.join(logDir, "qa.log");
14293
+ const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} qa >> ${shellQuote2(qaLog)} 2>&1 ${marker}`;
13283
14294
  entries.push(qaEntry);
13284
14295
  }
13285
14296
  const disableAudit = options?.noAudit === true || options?.audit === false;
13286
14297
  const installAudit = disableAudit ? false : config.audit.enabled;
13287
14298
  if (installAudit) {
13288
14299
  const auditSchedule = config.audit.schedule;
13289
- const auditLog = path30.join(logDir, "audit.log");
13290
- const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
14300
+ const auditLog = path32.join(logDir, "audit.log");
14301
+ const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} audit >> ${shellQuote2(auditLog)} 2>&1 ${marker}`;
13291
14302
  entries.push(auditEntry);
13292
14303
  }
14304
+ const disableUx = options?.noUx === true || options?.ux === false;
14305
+ const installUx = disableUx ? false : config.ux.enabled;
14306
+ if (installUx) {
14307
+ const uxSchedule = config.ux.schedule;
14308
+ const uxLog = path32.join(logDir, "ux.log");
14309
+ const uxEntry = `${uxSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} ux >> ${shellQuote2(uxLog)} 2>&1 ${marker}`;
14310
+ entries.push(uxEntry);
14311
+ }
13293
14312
  const disableAnalytics = options?.noAnalytics === true || options?.analytics === false;
13294
14313
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
13295
14314
  if (installAnalytics) {
13296
14315
  const analyticsSchedule = config.analytics.schedule;
13297
- const analyticsLog = path30.join(logDir, "analytics.log");
13298
- const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
14316
+ const analyticsLog = path32.join(logDir, "analytics.log");
14317
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} analytics >> ${shellQuote2(analyticsLog)} 2>&1 ${marker}`;
13299
14318
  entries.push(analyticsEntry);
13300
14319
  }
13301
14320
  const disablePrResolver = options?.noPrResolver === true || options?.prResolver === false;
13302
14321
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
13303
14322
  if (installPrResolver) {
13304
14323
  const prResolverSchedule = config.prResolver.schedule;
13305
- const prResolverLog = path30.join(logDir, "pr-resolver.log");
13306
- const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
14324
+ const prResolverLog = path32.join(logDir, "pr-resolver.log");
14325
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} resolve >> ${shellQuote2(prResolverLog)} 2>&1 ${marker}`;
13307
14326
  entries.push(prResolverEntry);
13308
14327
  }
13309
14328
  const disableMerger = options?.noMerger === true || options?.merger === false;
13310
14329
  const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
13311
14330
  if (installMerger) {
13312
14331
  const mergerSchedule = config.merger.schedule;
13313
- const mergerLog = path30.join(logDir, "merger.log");
13314
- const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
14332
+ const mergerLog = path32.join(logDir, "merger.log");
14333
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} merge >> ${shellQuote2(mergerLog)} 2>&1 ${marker}`;
13315
14334
  entries.push(mergerEntry);
13316
14335
  }
13317
14336
  const disableManager = options?.noManager === true || options?.manager === false;
13318
14337
  const installManager = disableManager ? false : config.manager?.enabled ?? false;
13319
14338
  if (installManager) {
13320
14339
  const managerSchedule = config.manager.schedule;
13321
- const managerLog = path30.join(logDir, `${MANAGER_LOG_NAME}.log`);
13322
- const managerEntry = `${managerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} manager >> ${shellQuote(managerLog)} 2>&1 ${marker}`;
14340
+ const managerLog = path32.join(logDir, `${MANAGER_LOG_NAME}.log`);
14341
+ const managerEntry = `${managerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} manager >> ${shellQuote2(managerLog)} 2>&1 ${marker}`;
13323
14342
  entries.push(managerEntry);
13324
14343
  }
13325
14344
  const existingEntries = new Set(
@@ -13340,7 +14359,7 @@ function performInstall(projectDir, config, options) {
13340
14359
  }
13341
14360
  }
13342
14361
  function installCommand(program2) {
13343
- 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("--no-pr-resolver", "Skip installing PR resolver cron").option("--no-merger", "Skip installing merger cron").option("--no-manager", "Skip installing manager cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
14362
+ 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-ux", "Skip installing UX cron").option("--no-analytics", "Skip installing analytics cron").option("--no-pr-resolver", "Skip installing PR resolver cron").option("--no-merger", "Skip installing merger cron").option("--no-manager", "Skip installing manager cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
13344
14363
  try {
13345
14364
  const projectDir = process.cwd();
13346
14365
  const config = loadConfig(projectDir);
@@ -13349,12 +14368,12 @@ function installCommand(program2) {
13349
14368
  const nightWatchBin = getNightWatchBinPath();
13350
14369
  const projectName = getProjectName(projectDir);
13351
14370
  const marker = generateMarker(projectName);
13352
- const logDir = path30.join(projectDir, LOG_DIR);
13353
- if (!fs29.existsSync(logDir)) {
13354
- fs29.mkdirSync(logDir, { recursive: true });
14371
+ const logDir = path32.join(projectDir, LOG_DIR);
14372
+ if (!fs31.existsSync(logDir)) {
14373
+ fs31.mkdirSync(logDir, { recursive: true });
13355
14374
  }
13356
- const executorLog = path30.join(logDir, "executor.log");
13357
- const reviewerLog = path30.join(logDir, "reviewer.log");
14375
+ const executorLog = path32.join(logDir, "executor.log");
14376
+ const reviewerLog = path32.join(logDir, "reviewer.log");
13358
14377
  const existingEntries = Array.from(
13359
14378
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
13360
14379
  );
@@ -13370,83 +14389,92 @@ function installCommand(program2) {
13370
14389
  const entries = [];
13371
14390
  const nodeBinDir = getNodeBinDir();
13372
14391
  const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
13373
- const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
14392
+ const cliBinPrefix = `export NW_CLI_BIN=${shellQuote2(nightWatchBin)} && `;
13374
14393
  const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
13375
14394
  let providerEnvPrefix = "";
13376
14395
  if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
13377
- const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
14396
+ const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote2(value)}`).join(" && ");
13378
14397
  providerEnvPrefix = exports + " && ";
13379
14398
  }
13380
14399
  const installExecutor = config.executorEnabled !== false;
13381
14400
  if (installExecutor) {
13382
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
14401
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} run >> ${shellQuote2(executorLog)} 2>&1 ${marker}`;
13383
14402
  entries.push(executorEntry);
13384
14403
  }
13385
14404
  const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
13386
14405
  if (installReviewer) {
13387
- const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
14406
+ const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} review >> ${shellQuote2(reviewerLog)} 2>&1 ${marker}`;
13388
14407
  entries.push(reviewerEntry);
13389
14408
  }
13390
14409
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
13391
14410
  let slicerLog;
13392
14411
  if (installSlicer) {
13393
- slicerLog = path30.join(logDir, "slicer.log");
14412
+ slicerLog = path32.join(logDir, "slicer.log");
13394
14413
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
13395
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
14414
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} planner >> ${shellQuote2(slicerLog)} 2>&1 ${marker}`;
13396
14415
  entries.push(slicerEntry);
13397
14416
  }
13398
14417
  const disableQa = options.noQa === true || options.qa === false;
13399
14418
  const installQa = disableQa ? false : config.qa.enabled;
13400
14419
  let qaLog;
13401
14420
  if (installQa) {
13402
- qaLog = path30.join(logDir, "qa.log");
14421
+ qaLog = path32.join(logDir, "qa.log");
13403
14422
  const qaSchedule = config.qa.schedule;
13404
- const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
14423
+ const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} qa >> ${shellQuote2(qaLog)} 2>&1 ${marker}`;
13405
14424
  entries.push(qaEntry);
13406
14425
  }
13407
14426
  const disableAudit = options.noAudit === true || options.audit === false;
13408
14427
  const installAudit = disableAudit ? false : config.audit.enabled;
13409
14428
  let auditLog;
13410
14429
  if (installAudit) {
13411
- auditLog = path30.join(logDir, "audit.log");
14430
+ auditLog = path32.join(logDir, "audit.log");
13412
14431
  const auditSchedule = config.audit.schedule;
13413
- const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
14432
+ const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} audit >> ${shellQuote2(auditLog)} 2>&1 ${marker}`;
13414
14433
  entries.push(auditEntry);
13415
14434
  }
14435
+ const disableUx = options.noUx === true || options.ux === false;
14436
+ const installUx = disableUx ? false : config.ux.enabled;
14437
+ let uxLog;
14438
+ if (installUx) {
14439
+ uxLog = path32.join(logDir, "ux.log");
14440
+ const uxSchedule = config.ux.schedule;
14441
+ const uxEntry = `${uxSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} ux >> ${shellQuote2(uxLog)} 2>&1 ${marker}`;
14442
+ entries.push(uxEntry);
14443
+ }
13416
14444
  const disableAnalytics = options.noAnalytics === true || options.analytics === false;
13417
14445
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
13418
14446
  let analyticsLog;
13419
14447
  if (installAnalytics) {
13420
- analyticsLog = path30.join(logDir, "analytics.log");
14448
+ analyticsLog = path32.join(logDir, "analytics.log");
13421
14449
  const analyticsSchedule = config.analytics.schedule;
13422
- const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
14450
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} analytics >> ${shellQuote2(analyticsLog)} 2>&1 ${marker}`;
13423
14451
  entries.push(analyticsEntry);
13424
14452
  }
13425
14453
  const disablePrResolver = options.noPrResolver === true || options.prResolver === false;
13426
14454
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
13427
14455
  let prResolverLog;
13428
14456
  if (installPrResolver) {
13429
- prResolverLog = path30.join(logDir, "pr-resolver.log");
14457
+ prResolverLog = path32.join(logDir, "pr-resolver.log");
13430
14458
  const prResolverSchedule = config.prResolver.schedule;
13431
- const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
14459
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} resolve >> ${shellQuote2(prResolverLog)} 2>&1 ${marker}`;
13432
14460
  entries.push(prResolverEntry);
13433
14461
  }
13434
14462
  const disableMerger = options.noMerger === true || options.merger === false;
13435
14463
  const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
13436
14464
  let mergerLog;
13437
14465
  if (installMerger) {
13438
- mergerLog = path30.join(logDir, "merger.log");
14466
+ mergerLog = path32.join(logDir, "merger.log");
13439
14467
  const mergerSchedule = config.merger.schedule;
13440
- const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
14468
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} merge >> ${shellQuote2(mergerLog)} 2>&1 ${marker}`;
13441
14469
  entries.push(mergerEntry);
13442
14470
  }
13443
14471
  const disableManager = options.noManager === true || options.manager === false;
13444
14472
  const installManager = disableManager ? false : config.manager?.enabled ?? false;
13445
14473
  let managerLog;
13446
14474
  if (installManager) {
13447
- managerLog = path30.join(logDir, `${MANAGER_LOG_NAME}.log`);
14475
+ managerLog = path32.join(logDir, `${MANAGER_LOG_NAME}.log`);
13448
14476
  const managerSchedule = config.manager.schedule;
13449
- const managerEntry = `${managerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} manager >> ${shellQuote(managerLog)} 2>&1 ${marker}`;
14477
+ const managerEntry = `${managerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote2(projectDir)} && ${shellQuote2(nightWatchBin)} manager >> ${shellQuote2(managerLog)} 2>&1 ${marker}`;
13450
14478
  entries.push(managerEntry);
13451
14479
  }
13452
14480
  const existingEntrySet = new Set(existingEntries);
@@ -13475,6 +14503,9 @@ function installCommand(program2) {
13475
14503
  if (installAudit && auditLog) {
13476
14504
  dim(` Audit: ${auditLog}`);
13477
14505
  }
14506
+ if (installUx && uxLog) {
14507
+ dim(` UX: ${uxLog}`);
14508
+ }
13478
14509
  if (installAnalytics && analyticsLog) {
13479
14510
  dim(` Analytics: ${analyticsLog}`);
13480
14511
  }
@@ -13501,8 +14532,8 @@ function installCommand(program2) {
13501
14532
 
13502
14533
  // src/commands/uninstall.ts
13503
14534
  init_dist();
13504
- import * as path31 from "path";
13505
- import * as fs30 from "fs";
14535
+ import * as path33 from "path";
14536
+ import * as fs32 from "fs";
13506
14537
  function performUninstall(projectDir, options) {
13507
14538
  try {
13508
14539
  const projectName = getProjectName(projectDir);
@@ -13517,8 +14548,8 @@ function performUninstall(projectDir, options) {
13517
14548
  const removedCount = removeEntriesForProject(projectDir, marker);
13518
14549
  unregisterProject(projectDir);
13519
14550
  if (!options?.keepLogs) {
13520
- const logDir = path31.join(projectDir, "logs");
13521
- if (fs30.existsSync(logDir)) {
14551
+ const logDir = path33.join(projectDir, "logs");
14552
+ if (fs32.existsSync(logDir)) {
13522
14553
  const logFiles = [
13523
14554
  "executor.log",
13524
14555
  "reviewer.log",
@@ -13528,15 +14559,15 @@ function performUninstall(projectDir, options) {
13528
14559
  "manager.log"
13529
14560
  ];
13530
14561
  logFiles.forEach((logFile) => {
13531
- const logPath = path31.join(logDir, logFile);
13532
- if (fs30.existsSync(logPath)) {
13533
- fs30.unlinkSync(logPath);
14562
+ const logPath = path33.join(logDir, logFile);
14563
+ if (fs32.existsSync(logPath)) {
14564
+ fs32.unlinkSync(logPath);
13534
14565
  }
13535
14566
  });
13536
14567
  try {
13537
- const remainingFiles = fs30.readdirSync(logDir);
14568
+ const remainingFiles = fs32.readdirSync(logDir);
13538
14569
  if (remainingFiles.length === 0) {
13539
- fs30.rmdirSync(logDir);
14570
+ fs32.rmdirSync(logDir);
13540
14571
  }
13541
14572
  } catch {
13542
14573
  }
@@ -13569,8 +14600,8 @@ function uninstallCommand(program2) {
13569
14600
  existingEntries.forEach((entry) => dim(` ${entry}`));
13570
14601
  const removedCount = removeEntriesForProject(projectDir, marker);
13571
14602
  if (!options.keepLogs) {
13572
- const logDir = path31.join(projectDir, "logs");
13573
- if (fs30.existsSync(logDir)) {
14603
+ const logDir = path33.join(projectDir, "logs");
14604
+ if (fs32.existsSync(logDir)) {
13574
14605
  const logFiles = [
13575
14606
  "executor.log",
13576
14607
  "reviewer.log",
@@ -13581,16 +14612,16 @@ function uninstallCommand(program2) {
13581
14612
  ];
13582
14613
  let logsRemoved = 0;
13583
14614
  logFiles.forEach((logFile) => {
13584
- const logPath = path31.join(logDir, logFile);
13585
- if (fs30.existsSync(logPath)) {
13586
- fs30.unlinkSync(logPath);
14615
+ const logPath = path33.join(logDir, logFile);
14616
+ if (fs32.existsSync(logPath)) {
14617
+ fs32.unlinkSync(logPath);
13587
14618
  logsRemoved++;
13588
14619
  }
13589
14620
  });
13590
14621
  try {
13591
- const remainingFiles = fs30.readdirSync(logDir);
14622
+ const remainingFiles = fs32.readdirSync(logDir);
13592
14623
  if (remainingFiles.length === 0) {
13593
- fs30.rmdirSync(logDir);
14624
+ fs32.rmdirSync(logDir);
13594
14625
  }
13595
14626
  } catch {
13596
14627
  }
@@ -13633,6 +14664,7 @@ function statusCommand(program2) {
13633
14664
  const reviewerProc = snapshot.processes.find((p) => p.name === "reviewer");
13634
14665
  const qaProc = snapshot.processes.find((p) => p.name === "qa");
13635
14666
  const auditProc = snapshot.processes.find((p) => p.name === "audit");
14667
+ const uxProc = snapshot.processes.find((p) => p.name === "ux");
13636
14668
  const plannerProc = snapshot.processes.find((p) => p.name === "planner");
13637
14669
  const analyticsProc = snapshot.processes.find((p) => p.name === "analytics");
13638
14670
  const mergerProc = snapshot.processes.find((p) => p.name === "merger");
@@ -13640,6 +14672,7 @@ function statusCommand(program2) {
13640
14672
  const reviewerLog = snapshot.logs.find((l) => l.name === "reviewer");
13641
14673
  const qaLog = snapshot.logs.find((l) => l.name === "qa");
13642
14674
  const auditLog = snapshot.logs.find((l) => l.name === "audit");
14675
+ const uxLog = snapshot.logs.find((l) => l.name === "ux");
13643
14676
  const plannerLog = snapshot.logs.find((l) => l.name === "planner");
13644
14677
  const analyticsLog = snapshot.logs.find((l) => l.name === "analytics");
13645
14678
  const mergerLog = snapshot.logs.find((l) => l.name === "merger");
@@ -13659,6 +14692,7 @@ function statusCommand(program2) {
13659
14692
  reviewer: { running: reviewerProc?.running ?? false, pid: reviewerProc?.pid ?? null },
13660
14693
  qa: { running: qaProc?.running ?? false, pid: qaProc?.pid ?? null },
13661
14694
  audit: { running: auditProc?.running ?? false, pid: auditProc?.pid ?? null },
14695
+ ux: { running: uxProc?.running ?? false, pid: uxProc?.pid ?? null },
13662
14696
  planner: { running: plannerProc?.running ?? false, pid: plannerProc?.pid ?? null },
13663
14697
  analytics: { running: analyticsProc?.running ?? false, pid: analyticsProc?.pid ?? null },
13664
14698
  merger: { running: mergerProc?.running ?? false, pid: mergerProc?.pid ?? null },
@@ -13690,6 +14724,12 @@ function statusCommand(program2) {
13690
14724
  exists: auditLog.exists,
13691
14725
  size: auditLog.size
13692
14726
  } : void 0,
14727
+ ux: uxLog ? {
14728
+ path: uxLog.path,
14729
+ lastLines: uxLog.lastLines,
14730
+ exists: uxLog.exists,
14731
+ size: uxLog.size
14732
+ } : void 0,
13693
14733
  planner: plannerLog ? {
13694
14734
  path: plannerLog.path,
13695
14735
  lastLines: plannerLog.lastLines,
@@ -13740,6 +14780,7 @@ function statusCommand(program2) {
13740
14780
  ]);
13741
14781
  processTable.push(["QA", formatRunningStatus(status.qa.running, status.qa.pid)]);
13742
14782
  processTable.push(["Audit", formatRunningStatus(status.audit.running, status.audit.pid)]);
14783
+ processTable.push(["UX", formatRunningStatus(status.ux.running, status.ux.pid)]);
13743
14784
  processTable.push([
13744
14785
  "Planner",
13745
14786
  formatRunningStatus(status.planner.running, status.planner.pid)
@@ -13801,6 +14842,13 @@ function statusCommand(program2) {
13801
14842
  status.logs.audit.exists ? "Exists" : "Not found"
13802
14843
  ]);
13803
14844
  }
14845
+ if (status.logs.ux) {
14846
+ logTable.push([
14847
+ "UX",
14848
+ status.logs.ux.exists ? formatBytes(status.logs.ux.size) : "-",
14849
+ status.logs.ux.exists ? "Exists" : "Not found"
14850
+ ]);
14851
+ }
13804
14852
  if (status.logs.planner) {
13805
14853
  logTable.push([
13806
14854
  "Planner",
@@ -13840,6 +14888,10 @@ function statusCommand(program2) {
13840
14888
  dim(" Audit last 5 lines:");
13841
14889
  status.logs.audit.lastLines.forEach((line) => dim(` ${line}`));
13842
14890
  }
14891
+ if (status.logs.ux?.exists && status.logs.ux.lastLines.length > 0) {
14892
+ dim(" UX last 5 lines:");
14893
+ status.logs.ux.lastLines.forEach((line) => dim(` ${line}`));
14894
+ }
13843
14895
  if (status.logs.planner?.exists && status.logs.planner.lastLines.length > 0) {
13844
14896
  dim(" Planner last 5 lines:");
13845
14897
  status.logs.planner.lastLines.forEach((line) => dim(` ${line}`));
@@ -13860,6 +14912,7 @@ function statusCommand(program2) {
13860
14912
  dim(" night-watch review - Run reviewer now");
13861
14913
  dim(" night-watch qa - Run QA now");
13862
14914
  dim(" night-watch audit - Run audit now");
14915
+ dim(" night-watch ux - Run UX agent now");
13863
14916
  dim(" night-watch planner - Run planner now");
13864
14917
  dim(" night-watch analytics - Run analytics now");
13865
14918
  dim(" night-watch merge - Run merger now");
@@ -13876,14 +14929,14 @@ function statusCommand(program2) {
13876
14929
  // src/commands/logs.ts
13877
14930
  init_dist();
13878
14931
  import { spawn as spawn3 } from "child_process";
13879
- import * as path32 from "path";
13880
- import * as fs31 from "fs";
14932
+ import * as path34 from "path";
14933
+ import * as fs33 from "fs";
13881
14934
  function getLastLines(filePath, lineCount) {
13882
- if (!fs31.existsSync(filePath)) {
14935
+ if (!fs33.existsSync(filePath)) {
13883
14936
  return `Log file not found: ${filePath}`;
13884
14937
  }
13885
14938
  try {
13886
- const content = fs31.readFileSync(filePath, "utf-8");
14939
+ const content = fs33.readFileSync(filePath, "utf-8");
13887
14940
  const lines = content.trim().split("\n");
13888
14941
  return lines.slice(-lineCount).join("\n");
13889
14942
  } catch (error2) {
@@ -13891,7 +14944,7 @@ function getLastLines(filePath, lineCount) {
13891
14944
  }
13892
14945
  }
13893
14946
  function followLog(filePath) {
13894
- if (!fs31.existsSync(filePath)) {
14947
+ if (!fs33.existsSync(filePath)) {
13895
14948
  console.log(`Log file not found: ${filePath}`);
13896
14949
  console.log("The log file will be created when the first execution runs.");
13897
14950
  return;
@@ -13910,26 +14963,28 @@ function followLog(filePath) {
13910
14963
  function logsCommand(program2) {
13911
14964
  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(
13912
14965
  "-t, --type <type>",
13913
- "Log type to view (executor|reviewer|qa|audit|planner|analytics|merger|manager|all)",
14966
+ "Log type to view (executor|reviewer|qa|audit|ux|planner|analytics|merger|manager|all)",
13914
14967
  "all"
13915
14968
  ).action(async (options) => {
13916
14969
  try {
13917
14970
  const projectDir = process.cwd();
13918
- const logDir = path32.join(projectDir, LOG_DIR);
14971
+ const logDir = path34.join(projectDir, LOG_DIR);
13919
14972
  const lineCount = parseInt(options.lines || "50", 10);
13920
- const executorLog = path32.join(logDir, EXECUTOR_LOG_FILE);
13921
- const reviewerLog = path32.join(logDir, REVIEWER_LOG_FILE);
13922
- const qaLog = path32.join(logDir, `${QA_LOG_NAME}.log`);
13923
- const auditLog = path32.join(logDir, `${AUDIT_LOG_NAME}.log`);
13924
- const plannerLog = path32.join(logDir, `${PLANNER_LOG_NAME}.log`);
13925
- const analyticsLog = path32.join(logDir, `${ANALYTICS_LOG_NAME}.log`);
13926
- const mergerLog = path32.join(logDir, `${MERGER_LOG_NAME}.log`);
13927
- const managerLog = path32.join(logDir, `${MANAGER_LOG_NAME}.log`);
14973
+ const executorLog = path34.join(logDir, EXECUTOR_LOG_FILE);
14974
+ const reviewerLog = path34.join(logDir, REVIEWER_LOG_FILE);
14975
+ const qaLog = path34.join(logDir, `${QA_LOG_NAME}.log`);
14976
+ const auditLog = path34.join(logDir, `${AUDIT_LOG_NAME}.log`);
14977
+ const uxLog = path34.join(logDir, `${UX_LOG_NAME}.log`);
14978
+ const plannerLog = path34.join(logDir, `${PLANNER_LOG_NAME}.log`);
14979
+ const analyticsLog = path34.join(logDir, `${ANALYTICS_LOG_NAME}.log`);
14980
+ const mergerLog = path34.join(logDir, `${MERGER_LOG_NAME}.log`);
14981
+ const managerLog = path34.join(logDir, `${MANAGER_LOG_NAME}.log`);
13928
14982
  const logType = options.type?.toLowerCase() || "all";
13929
14983
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
13930
14984
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
13931
14985
  const showQa = logType === "all" || logType === "qa";
13932
14986
  const showAudit = logType === "all" || logType === "audit";
14987
+ const showUx = logType === "all" || logType === "ux";
13933
14988
  const showPlanner = logType === "all" || logType === "planner" || logType === "slice" || logType === "slicer";
13934
14989
  const showAnalytics = logType === "all" || logType === "analytics";
13935
14990
  const showMerger = logType === "all" || logType === "merge" || logType === "merger";
@@ -13937,12 +14992,15 @@ function logsCommand(program2) {
13937
14992
  if (options.follow) {
13938
14993
  if (logType === "all") {
13939
14994
  dim("Note: Following all logs is not supported. Showing executor log.");
13940
- dim("Use --type reviewer|qa|audit|planner|analytics|merger|manager for other logs.\n");
14995
+ dim(
14996
+ "Use --type reviewer|qa|audit|ux|planner|analytics|merger|manager for other logs.\n"
14997
+ );
13941
14998
  }
13942
14999
  let targetLog = executorLog;
13943
15000
  if (showReviewer) targetLog = reviewerLog;
13944
15001
  else if (showQa) targetLog = qaLog;
13945
15002
  else if (showAudit) targetLog = auditLog;
15003
+ else if (showUx) targetLog = uxLog;
13946
15004
  else if (showPlanner) targetLog = plannerLog;
13947
15005
  else if (showAnalytics) targetLog = analyticsLog;
13948
15006
  else if (showMerger) targetLog = mergerLog;
@@ -13975,6 +15033,12 @@ function logsCommand(program2) {
13975
15033
  console.log();
13976
15034
  console.log(getLastLines(auditLog, lineCount));
13977
15035
  }
15036
+ if (showUx) {
15037
+ header("UX Log");
15038
+ dim(`File: ${uxLog}`);
15039
+ console.log();
15040
+ console.log(getLastLines(uxLog, lineCount));
15041
+ }
13978
15042
  if (showPlanner) {
13979
15043
  header("Planner Log");
13980
15044
  dim(`File: ${plannerLog}`);
@@ -14003,7 +15067,7 @@ function logsCommand(program2) {
14003
15067
  dim("---");
14004
15068
  dim("Tip: Use -f to follow logs in real-time");
14005
15069
  dim(
14006
- " Use --type executor|reviewer|qa|audit|planner|analytics|merger|manager to view specific logs"
15070
+ " Use --type executor|reviewer|qa|audit|ux|planner|analytics|merger|manager to view specific logs"
14007
15071
  );
14008
15072
  } catch (err) {
14009
15073
  console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
@@ -14015,30 +15079,30 @@ function logsCommand(program2) {
14015
15079
  // src/commands/prd.ts
14016
15080
  init_dist();
14017
15081
  import { execSync as execSync5, spawn as spawn4, spawnSync } from "child_process";
14018
- import * as fs32 from "fs";
14019
- import * as path33 from "path";
15082
+ import * as fs34 from "fs";
15083
+ import * as path35 from "path";
14020
15084
  import { fileURLToPath as fileURLToPath4 } from "url";
14021
- import { dirname as dirname10 } from "path";
15085
+ import { dirname as dirname11 } from "path";
14022
15086
  var __filename3 = fileURLToPath4(import.meta.url);
14023
- var __dirname3 = dirname10(__filename3);
15087
+ var __dirname3 = dirname11(__filename3);
14024
15088
  function findTemplatesDir2(startDir) {
14025
15089
  let current = startDir;
14026
15090
  for (let i = 0; i < 8; i++) {
14027
- const candidate = path33.join(current, "templates");
14028
- if (fs32.existsSync(candidate) && fs32.statSync(candidate).isDirectory()) {
15091
+ const candidate = path35.join(current, "templates");
15092
+ if (fs34.existsSync(candidate) && fs34.statSync(candidate).isDirectory()) {
14029
15093
  return candidate;
14030
15094
  }
14031
- current = path33.dirname(current);
15095
+ current = path35.dirname(current);
14032
15096
  }
14033
- return path33.join(startDir, "templates");
15097
+ return path35.join(startDir, "templates");
14034
15098
  }
14035
15099
  var TEMPLATES_DIR2 = findTemplatesDir2(__dirname3);
14036
15100
  function slugify2(name) {
14037
15101
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
14038
15102
  }
14039
15103
  function getNextPrdNumber2(prdDir) {
14040
- if (!fs32.existsSync(prdDir)) return 1;
14041
- const files = fs32.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
15104
+ if (!fs34.existsSync(prdDir)) return 1;
15105
+ const files = fs34.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
14042
15106
  const numbers = files.map((f) => {
14043
15107
  const match = f.match(/^(\d+)-/);
14044
15108
  return match ? parseInt(match[1], 10) : 0;
@@ -14119,13 +15183,13 @@ function resolveGitHubBlobUrl(projectDir, relPath) {
14119
15183
  return null;
14120
15184
  }
14121
15185
  const ref = branch && branch !== "HEAD" ? branch : "main";
14122
- return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path33.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
15186
+ return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path35.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
14123
15187
  } catch {
14124
15188
  return null;
14125
15189
  }
14126
15190
  }
14127
15191
  function buildGithubIssueBody(prdPath, projectDir, prdContent) {
14128
- const relPath = path33.relative(projectDir, prdPath);
15192
+ const relPath = path35.relative(projectDir, prdPath);
14129
15193
  const blobUrl = resolveGitHubBlobUrl(projectDir, relPath);
14130
15194
  const fileLine = blobUrl ? `PRD file: [\`${relPath}\`](${blobUrl})` : `PRD file: \`${relPath}\``;
14131
15195
  return `${fileLine}
@@ -14136,13 +15200,13 @@ ${prdContent}
14136
15200
  Created via \`night-watch prd create\`.`;
14137
15201
  }
14138
15202
  async function generatePrdWithClaude(description, projectDir, model) {
14139
- const bundledTemplatePath = path33.join(TEMPLATES_DIR2, "prd-creator.md");
14140
- const installedTemplatePath = path33.join(projectDir, "instructions", "prd-creator.md");
14141
- const templatePath = fs32.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
14142
- if (!fs32.existsSync(templatePath)) {
15203
+ const bundledTemplatePath = path35.join(TEMPLATES_DIR2, "prd-creator.md");
15204
+ const installedTemplatePath = path35.join(projectDir, "instructions", "prd-creator.md");
15205
+ const templatePath = fs34.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
15206
+ if (!fs34.existsSync(templatePath)) {
14143
15207
  return null;
14144
15208
  }
14145
- const planningPrinciples = fs32.readFileSync(templatePath, "utf-8");
15209
+ const planningPrinciples = fs34.readFileSync(templatePath, "utf-8");
14146
15210
  const prompt = buildPrdPrompt(description, projectDir, planningPrinciples);
14147
15211
  const modelId = model ?? CLAUDE_MODEL_IDS.opus;
14148
15212
  const env = buildNativeClaudeEnv(process.env);
@@ -14210,17 +15274,17 @@ function runGh(args, cwd) {
14210
15274
  return null;
14211
15275
  }
14212
15276
  function createGithubIssue(title, prdPath, projectDir, prdContent) {
14213
- const tmpFile = path33.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
15277
+ const tmpFile = path35.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
14214
15278
  try {
14215
15279
  const body = buildGithubIssueBody(prdPath, projectDir, prdContent);
14216
- fs32.writeFileSync(tmpFile, body, "utf-8");
15280
+ fs34.writeFileSync(tmpFile, body, "utf-8");
14217
15281
  const baseArgs = ["issue", "create", "--title", `PRD: ${title}`, "--body-file", tmpFile];
14218
15282
  return runGh([...baseArgs, "--label", "prd"], projectDir) ?? runGh(baseArgs, projectDir);
14219
15283
  } catch {
14220
15284
  return null;
14221
15285
  } finally {
14222
15286
  try {
14223
- fs32.unlinkSync(tmpFile);
15287
+ fs34.unlinkSync(tmpFile);
14224
15288
  } catch {
14225
15289
  }
14226
15290
  }
@@ -14233,10 +15297,10 @@ function parseDependencies(content) {
14233
15297
  function isClaimActive(claimPath, maxRuntime) {
14234
15298
  const claimStaleAfter = maxRuntime > 0 ? maxRuntime : 14400;
14235
15299
  try {
14236
- if (!fs32.existsSync(claimPath)) {
15300
+ if (!fs34.existsSync(claimPath)) {
14237
15301
  return { active: false };
14238
15302
  }
14239
- const content = fs32.readFileSync(claimPath, "utf-8");
15303
+ const content = fs34.readFileSync(claimPath, "utf-8");
14240
15304
  const claim = JSON.parse(content);
14241
15305
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
14242
15306
  if (age < claimStaleAfter) {
@@ -14251,9 +15315,9 @@ function prdCommand(program2) {
14251
15315
  const prd = program2.command("prd").description("Manage PRD files");
14252
15316
  prd.command("create").description("Generate a new PRD markdown file using Claude").argument("<name>", "PRD description").option("--number", "Add auto-numbering prefix to the filename", false).option("--model <model>", "Claude model to use (e.g. sonnet, opus, or a full model ID)").action(async (name, options) => {
14253
15317
  const projectDir = process.cwd();
14254
- const prdDir = path33.join(projectDir, resolvePrdCreateDir());
14255
- if (!fs32.existsSync(prdDir)) {
14256
- fs32.mkdirSync(prdDir, { recursive: true });
15318
+ const prdDir = path35.join(projectDir, resolvePrdCreateDir());
15319
+ if (!fs34.existsSync(prdDir)) {
15320
+ fs34.mkdirSync(prdDir, { recursive: true });
14257
15321
  }
14258
15322
  const resolvedModel = options.model ? CLAUDE_MODEL_IDS[options.model] ?? options.model : void 0;
14259
15323
  const modelLabel = resolvedModel ?? CLAUDE_MODEL_IDS.opus;
@@ -14269,13 +15333,13 @@ function prdCommand(program2) {
14269
15333
  const prdTitle = extractPrdTitle(generated) ?? name;
14270
15334
  const slug = slugify2(prdTitle);
14271
15335
  const filename = options.number ? `${String(getNextPrdNumber2(prdDir)).padStart(2, "0")}-${slug}.md` : `${slug}.md`;
14272
- const filePath = path33.join(prdDir, filename);
14273
- if (fs32.existsSync(filePath)) {
15336
+ const filePath = path35.join(prdDir, filename);
15337
+ if (fs34.existsSync(filePath)) {
14274
15338
  error(`File already exists: ${filePath}`);
14275
15339
  dim("Use a different name or remove the existing file.");
14276
15340
  process.exit(1);
14277
15341
  }
14278
- fs32.writeFileSync(filePath, generated, "utf-8");
15342
+ fs34.writeFileSync(filePath, generated, "utf-8");
14279
15343
  header("PRD Created");
14280
15344
  success(`Created: ${filePath}`);
14281
15345
  const issueUrl = createGithubIssue(prdTitle, filePath, projectDir, generated);
@@ -14288,15 +15352,15 @@ function prdCommand(program2) {
14288
15352
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
14289
15353
  const projectDir = process.cwd();
14290
15354
  const config = loadConfig(projectDir);
14291
- const absolutePrdDir = path33.join(projectDir, config.prdDir);
14292
- const doneDir = path33.join(absolutePrdDir, "done");
15355
+ const absolutePrdDir = path35.join(projectDir, config.prdDir);
15356
+ const doneDir = path35.join(absolutePrdDir, "done");
14293
15357
  const pending = [];
14294
- if (fs32.existsSync(absolutePrdDir)) {
14295
- const files = fs32.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
15358
+ if (fs34.existsSync(absolutePrdDir)) {
15359
+ const files = fs34.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
14296
15360
  for (const file of files) {
14297
- const content = fs32.readFileSync(path33.join(absolutePrdDir, file), "utf-8");
15361
+ const content = fs34.readFileSync(path35.join(absolutePrdDir, file), "utf-8");
14298
15362
  const deps = parseDependencies(content);
14299
- const claimPath = path33.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
15363
+ const claimPath = path35.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
14300
15364
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
14301
15365
  pending.push({
14302
15366
  name: file,
@@ -14307,10 +15371,10 @@ function prdCommand(program2) {
14307
15371
  }
14308
15372
  }
14309
15373
  const done = [];
14310
- if (fs32.existsSync(doneDir)) {
14311
- const files = fs32.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
15374
+ if (fs34.existsSync(doneDir)) {
15375
+ const files = fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14312
15376
  for (const file of files) {
14313
- const content = fs32.readFileSync(path33.join(doneDir, file), "utf-8");
15377
+ const content = fs34.readFileSync(path35.join(doneDir, file), "utf-8");
14314
15378
  const deps = parseDependencies(content);
14315
15379
  done.push({ name: file, dependencies: deps });
14316
15380
  }
@@ -14347,7 +15411,7 @@ import blessed6 from "blessed";
14347
15411
  // src/commands/dashboard/tab-status.ts
14348
15412
  init_dist();
14349
15413
  import blessed from "blessed";
14350
- import * as fs33 from "fs";
15414
+ import * as fs35 from "fs";
14351
15415
  function sortPrdsByPriority(prds, priority) {
14352
15416
  if (priority.length === 0) return prds;
14353
15417
  const priorityMap = /* @__PURE__ */ new Map();
@@ -14443,7 +15507,7 @@ function renderLogPane(projectDir, logs) {
14443
15507
  let newestMtime = 0;
14444
15508
  for (const log of existingLogs) {
14445
15509
  try {
14446
- const stat = fs33.statSync(log.path);
15510
+ const stat = fs35.statSync(log.path);
14447
15511
  if (stat.mtimeMs > newestMtime) {
14448
15512
  newestMtime = stat.mtimeMs;
14449
15513
  newestLog = log;
@@ -16096,8 +17160,8 @@ function createActionsTab() {
16096
17160
  // src/commands/dashboard/tab-logs.ts
16097
17161
  init_dist();
16098
17162
  import blessed5 from "blessed";
16099
- import * as fs34 from "fs";
16100
- import * as path34 from "path";
17163
+ import * as fs36 from "fs";
17164
+ import * as path36 from "path";
16101
17165
  var LOG_NAMES = ["executor", "reviewer"];
16102
17166
  var LOG_LINES = 200;
16103
17167
  function createLogsTab() {
@@ -16138,7 +17202,7 @@ function createLogsTab() {
16138
17202
  let activeKeyHandlers = [];
16139
17203
  let activeCtx = null;
16140
17204
  function getLogPath(projectDir, logName) {
16141
- return path34.join(projectDir, "logs", `${logName}.log`);
17205
+ return path36.join(projectDir, "logs", `${logName}.log`);
16142
17206
  }
16143
17207
  function updateSelector() {
16144
17208
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -16152,7 +17216,7 @@ function createLogsTab() {
16152
17216
  function loadLog(ctx) {
16153
17217
  const logName = LOG_NAMES[selectedLogIndex];
16154
17218
  const logPath = getLogPath(ctx.projectDir, logName);
16155
- if (!fs34.existsSync(logPath)) {
17219
+ if (!fs36.existsSync(logPath)) {
16156
17220
  logContent.setContent(
16157
17221
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
16158
17222
 
@@ -16162,7 +17226,7 @@ Log will appear here once the ${logName} runs.`
16162
17226
  return;
16163
17227
  }
16164
17228
  try {
16165
- const stat = fs34.statSync(logPath);
17229
+ const stat = fs36.statSync(logPath);
16166
17230
  const sizeKB = (stat.size / 1024).toFixed(1);
16167
17231
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
16168
17232
  } catch {
@@ -16487,6 +17551,15 @@ function dashboardCommand(program2) {
16487
17551
  // src/commands/doctor.ts
16488
17552
  init_dist();
16489
17553
  init_dist();
17554
+ function resolveDoctorErrorCategory(configPassed, providerPassed) {
17555
+ if (!configPassed) {
17556
+ return "config";
17557
+ }
17558
+ if (!providerPassed) {
17559
+ return "provider";
17560
+ }
17561
+ return "unknown";
17562
+ }
16490
17563
  function runCheck(checkNum, total, checkName, checkFn, options) {
16491
17564
  step(checkNum, total, `Checking ${checkName}...`);
16492
17565
  const result = checkFn();
@@ -16605,6 +17678,12 @@ function doctorCommand(program2) {
16605
17678
  success("All checks passed");
16606
17679
  } else {
16607
17680
  error("Issues found \u2014 fix errors above before running Night Watch");
17681
+ fireTelemetryEvent("doctor_failed", {
17682
+ command: "doctor",
17683
+ errorCategory: resolveDoctorErrorCategory(configResult.passed, providerResult.passed),
17684
+ success: false,
17685
+ failure: true
17686
+ });
16608
17687
  process.exit(1);
16609
17688
  }
16610
17689
  });
@@ -16612,13 +17691,13 @@ function doctorCommand(program2) {
16612
17691
 
16613
17692
  // src/commands/serve.ts
16614
17693
  init_dist();
16615
- import * as fs39 from "fs";
17694
+ import * as fs41 from "fs";
16616
17695
 
16617
17696
  // ../server/dist/index.js
16618
17697
  init_dist();
16619
- import * as fs38 from "fs";
16620
- import * as path40 from "path";
16621
- import { dirname as dirname12 } from "path";
17698
+ import * as fs40 from "fs";
17699
+ import * as path42 from "path";
17700
+ import { dirname as dirname13 } from "path";
16622
17701
  import { fileURLToPath as fileURLToPath5 } from "url";
16623
17702
  import cors from "cors";
16624
17703
  import express from "express";
@@ -16702,8 +17781,8 @@ function setupGracefulShutdown(server, beforeClose) {
16702
17781
 
16703
17782
  // ../server/dist/middleware/project-resolver.middleware.js
16704
17783
  init_dist();
16705
- import * as fs35 from "fs";
16706
- import * as path35 from "path";
17784
+ import * as fs37 from "fs";
17785
+ import * as path37 from "path";
16707
17786
  function resolveProject(req, res, next) {
16708
17787
  const projectId = req.params.projectId;
16709
17788
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -16713,7 +17792,7 @@ function resolveProject(req, res, next) {
16713
17792
  res.status(404).json({ error: `Project not found: ${decodedId}` });
16714
17793
  return;
16715
17794
  }
16716
- if (!fs35.existsSync(entry.path) || !fs35.existsSync(path35.join(entry.path, CONFIG_FILE_NAME))) {
17795
+ if (!fs37.existsSync(entry.path) || !fs37.existsSync(path37.join(entry.path, CONFIG_FILE_NAME))) {
16717
17796
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
16718
17797
  return;
16719
17798
  }
@@ -16756,10 +17835,128 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
16756
17835
  }, 2e3);
16757
17836
  }
16758
17837
 
17838
+ // ../server/dist/global-startup-summary.js
17839
+ init_dist();
17840
+ var RESET = "\x1B[0m";
17841
+ var DIM = "\x1B[2m";
17842
+ var BOLD = "\x1B[1m";
17843
+ var GREEN = "\x1B[32m";
17844
+ var YELLOW = "\x1B[33m";
17845
+ var RED = "\x1B[31m";
17846
+ var CYAN = "\x1B[36m";
17847
+ var FAILURE_OUTCOME_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
17848
+ function paint(enabled, code, value) {
17849
+ return enabled ? `${code}${value}${RESET}` : value;
17850
+ }
17851
+ function palette(color) {
17852
+ return {
17853
+ bold: (value) => paint(color, BOLD, value),
17854
+ dim: (value) => paint(color, DIM, value),
17855
+ green: (value) => paint(color, GREEN, value),
17856
+ yellow: (value) => paint(color, YELLOW, value),
17857
+ red: (value) => paint(color, RED, value),
17858
+ cyan: (value) => paint(color, CYAN, value)
17859
+ };
17860
+ }
17861
+ function shouldUseColor() {
17862
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
17863
+ }
17864
+ function failureRateFromSummary(summary) {
17865
+ if (summary.totalCount === 0)
17866
+ return null;
17867
+ const failed = summary.failureCount + summary.timeoutCount + summary.rateLimitedCount;
17868
+ return Math.round(failed / summary.totalCount * 100);
17869
+ }
17870
+ function getExecutorStatus(config, snapshot) {
17871
+ if (snapshot.processes.some((processInfo) => processInfo.running)) {
17872
+ return "running";
17873
+ }
17874
+ const executorActive = snapshot.crontab.installed && config.executorEnabled !== false && !config.pausedJobs?.executor;
17875
+ return executorActive ? "active" : "paused";
17876
+ }
17877
+ function describeError(error2) {
17878
+ const message = error2 instanceof Error ? error2.message : String(error2);
17879
+ return message.split("\n")[0]?.trim() || "unknown error";
17880
+ }
17881
+ function formatStatusMarker(status, colors) {
17882
+ if (status === "error")
17883
+ return colors.red("x");
17884
+ if (status === "paused")
17885
+ return colors.yellow("\u25CB");
17886
+ return colors.green("\u25CF");
17887
+ }
17888
+ function buildProjectStartupSummary(entry, config, snapshot, outcomeSummary) {
17889
+ return {
17890
+ name: entry.name,
17891
+ path: entry.path,
17892
+ status: getExecutorStatus(config, snapshot),
17893
+ readyPrds: snapshot.prds.filter((prd) => prd.status === "ready").length,
17894
+ openPrs: snapshot.prs.length,
17895
+ failedPrs: snapshot.prs.filter((pr) => pr.ciStatus === "fail").length,
17896
+ pendingPrs: snapshot.prs.filter((pr) => pr.ciStatus === "pending").length,
17897
+ failureRate: failureRateFromSummary(outcomeSummary),
17898
+ provider: String(config.provider),
17899
+ cronInstalled: snapshot.crontab.installed,
17900
+ runningProcesses: snapshot.processes.filter((processInfo) => processInfo.running).map((processInfo) => processInfo.name)
17901
+ };
17902
+ }
17903
+ async function collectProjectStartupSummary(entry) {
17904
+ try {
17905
+ const config = loadConfig(entry.path);
17906
+ const snapshot = await fetchStatusSnapshot(entry.path, config);
17907
+ const fromFinishedAt = Date.now() - FAILURE_OUTCOME_WINDOW_MS;
17908
+ const outcomeSummary = getRepositories().sessionOutcomes.querySummary({
17909
+ projectPath: entry.path,
17910
+ fromFinishedAt
17911
+ });
17912
+ return buildProjectStartupSummary(entry, config, snapshot, outcomeSummary);
17913
+ } catch (error2) {
17914
+ return {
17915
+ name: entry.name,
17916
+ path: entry.path,
17917
+ status: "error",
17918
+ readyPrds: 0,
17919
+ openPrs: 0,
17920
+ failedPrs: 0,
17921
+ pendingPrs: 0,
17922
+ failureRate: null,
17923
+ provider: "n/a",
17924
+ cronInstalled: false,
17925
+ runningProcesses: [],
17926
+ error: describeError(error2)
17927
+ };
17928
+ }
17929
+ }
17930
+ function formatProjectStartupSummaryLine(summary, options = {}) {
17931
+ const color = options.color ?? shouldUseColor();
17932
+ const c = palette(color);
17933
+ const statusColor = summary.status === "active" || summary.status === "running" ? c.green : c.yellow;
17934
+ const statusText = summary.status === "error" ? c.red("error") : statusColor(summary.status.padEnd(7, " "));
17935
+ const marker = formatStatusMarker(summary.status, c);
17936
+ if (summary.status === "error") {
17937
+ return ` ${marker} ${statusText} ${c.bold(summary.name)} ${c.red(summary.error ?? "unknown error")} ${c.dim(summary.path)}`;
17938
+ }
17939
+ const prStatus = summary.failedPrs > 0 || summary.pendingPrs > 0 ? ` (${summary.failedPrs} fail, ${summary.pendingPrs} pending)` : "";
17940
+ const failureText = summary.failureRate === null ? c.dim("fail n/a") : `fail ${c.cyan(`${summary.failureRate}%`)}`;
17941
+ const cronText = summary.cronInstalled ? c.green("cron on") : c.dim("cron off");
17942
+ const runningText = summary.runningProcesses.length > 0 ? ` ${c.dim(`run ${summary.runningProcesses.join(",")}`)}` : "";
17943
+ return [
17944
+ ` ${marker}`,
17945
+ statusText,
17946
+ c.bold(summary.name),
17947
+ `ready ${c.cyan(String(summary.readyPrds))}`,
17948
+ `PRs ${c.cyan(String(summary.openPrs))}${prStatus}`,
17949
+ failureText,
17950
+ `provider ${c.cyan(summary.provider)}`,
17951
+ cronText,
17952
+ `${c.dim(summary.path)}${runningText}`
17953
+ ].join(" ");
17954
+ }
17955
+
16759
17956
  // ../server/dist/routes/action.routes.js
16760
17957
  init_dist();
16761
- import * as fs36 from "fs";
16762
- import * as path36 from "path";
17958
+ import * as fs38 from "fs";
17959
+ import * as path38 from "path";
16763
17960
  import { execSync as execSync6, spawn as spawn6 } from "child_process";
16764
17961
  import { Router } from "express";
16765
17962
 
@@ -16797,17 +17994,17 @@ function getBoardProvider(config, projectDir) {
16797
17994
  function cleanOrphanedClaims(dir) {
16798
17995
  let entries;
16799
17996
  try {
16800
- entries = fs36.readdirSync(dir, { withFileTypes: true });
17997
+ entries = fs38.readdirSync(dir, { withFileTypes: true });
16801
17998
  } catch {
16802
17999
  return;
16803
18000
  }
16804
18001
  for (const entry of entries) {
16805
- const fullPath = path36.join(dir, entry.name);
18002
+ const fullPath = path38.join(dir, entry.name);
16806
18003
  if (entry.isDirectory() && entry.name !== "done") {
16807
18004
  cleanOrphanedClaims(fullPath);
16808
18005
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
16809
18006
  try {
16810
- fs36.unlinkSync(fullPath);
18007
+ fs38.unlinkSync(fullPath);
16811
18008
  } catch {
16812
18009
  }
16813
18010
  }
@@ -16962,19 +18159,19 @@ function createActionRouteHandlers(ctx) {
16962
18159
  res.status(400).json({ error: "Invalid PRD name" });
16963
18160
  return;
16964
18161
  }
16965
- const prdDir = path36.join(projectDir, config.prdDir);
18162
+ const prdDir = path38.join(projectDir, config.prdDir);
16966
18163
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
16967
- const pendingPath = path36.join(prdDir, normalized);
16968
- const donePath = path36.join(prdDir, "done", normalized);
16969
- if (fs36.existsSync(pendingPath)) {
18164
+ const pendingPath = path38.join(prdDir, normalized);
18165
+ const donePath = path38.join(prdDir, "done", normalized);
18166
+ if (fs38.existsSync(pendingPath)) {
16970
18167
  res.json({ message: `"${normalized}" is already pending` });
16971
18168
  return;
16972
18169
  }
16973
- if (!fs36.existsSync(donePath)) {
18170
+ if (!fs38.existsSync(donePath)) {
16974
18171
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
16975
18172
  return;
16976
18173
  }
16977
- fs36.renameSync(donePath, pendingPath);
18174
+ fs38.renameSync(donePath, pendingPath);
16978
18175
  res.json({ message: `Moved "${normalized}" back to pending` });
16979
18176
  } catch (error2) {
16980
18177
  res.status(500).json({
@@ -16992,11 +18189,11 @@ function createActionRouteHandlers(ctx) {
16992
18189
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
16993
18190
  return;
16994
18191
  }
16995
- if (fs36.existsSync(lockPath)) {
16996
- fs36.unlinkSync(lockPath);
18192
+ if (fs38.existsSync(lockPath)) {
18193
+ fs38.unlinkSync(lockPath);
16997
18194
  }
16998
- const prdDir = path36.join(projectDir, config.prdDir);
16999
- if (fs36.existsSync(prdDir)) {
18195
+ const prdDir = path38.join(projectDir, config.prdDir);
18196
+ if (fs38.existsSync(prdDir)) {
17000
18197
  cleanOrphanedClaims(prdDir);
17001
18198
  }
17002
18199
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -17367,6 +18564,12 @@ function validateConfigChanges(changes, currentConfig) {
17367
18564
  if (changes.autoMerge !== void 0 && typeof changes.autoMerge !== "boolean") {
17368
18565
  return "autoMerge must be a boolean";
17369
18566
  }
18567
+ if (changes.modelAttribution !== void 0 && typeof changes.modelAttribution !== "boolean") {
18568
+ return "modelAttribution must be a boolean";
18569
+ }
18570
+ if (changes.newPrLabel !== void 0 && typeof changes.newPrLabel !== "string") {
18571
+ return "newPrLabel must be a string";
18572
+ }
17370
18573
  if (changes.autoMergeMethod !== void 0) {
17371
18574
  const validMethods = ["squash", "merge", "rebase"];
17372
18575
  if (!validMethods.includes(changes.autoMergeMethod)) {
@@ -17764,8 +18967,8 @@ function createProjectConfigRoutes() {
17764
18967
 
17765
18968
  // ../server/dist/routes/doctor.routes.js
17766
18969
  init_dist();
17767
- import * as fs37 from "fs";
17768
- import * as path37 from "path";
18970
+ import * as fs39 from "fs";
18971
+ import * as path39 from "path";
17769
18972
  import { execSync as execSync7 } from "child_process";
17770
18973
  import { Router as Router4 } from "express";
17771
18974
  function runDoctorChecks(projectDir, config) {
@@ -17798,7 +19001,7 @@ function runDoctorChecks(projectDir, config) {
17798
19001
  });
17799
19002
  }
17800
19003
  try {
17801
- const projectName = path37.basename(projectDir);
19004
+ const projectName = path39.basename(projectDir);
17802
19005
  const marker = generateMarker(projectName);
17803
19006
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
17804
19007
  if (crontabEntries.length > 0) {
@@ -17821,8 +19024,8 @@ function runDoctorChecks(projectDir, config) {
17821
19024
  detail: "Failed to check crontab"
17822
19025
  });
17823
19026
  }
17824
- const configPath = path37.join(projectDir, CONFIG_FILE_NAME);
17825
- if (fs37.existsSync(configPath)) {
19027
+ const configPath = path39.join(projectDir, CONFIG_FILE_NAME);
19028
+ if (fs39.existsSync(configPath)) {
17826
19029
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
17827
19030
  } else {
17828
19031
  checks.push({
@@ -17831,9 +19034,9 @@ function runDoctorChecks(projectDir, config) {
17831
19034
  detail: "Config file not found (using defaults)"
17832
19035
  });
17833
19036
  }
17834
- const prdDir = path37.join(projectDir, config.prdDir);
17835
- if (fs37.existsSync(prdDir)) {
17836
- const prds = fs37.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
19037
+ const prdDir = path39.join(projectDir, config.prdDir);
19038
+ if (fs39.existsSync(prdDir)) {
19039
+ const prds = fs39.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
17837
19040
  checks.push({
17838
19041
  name: "prdDir",
17839
19042
  status: "pass",
@@ -18083,7 +19286,7 @@ function createProjectFeedbackRoutes() {
18083
19286
  // ../server/dist/routes/job.routes.js
18084
19287
  init_dist();
18085
19288
  import { spawn as spawn7 } from "child_process";
18086
- import { createHmac, randomUUID, timingSafeEqual } from "crypto";
19289
+ import { createHmac, randomUUID as randomUUID2, timingSafeEqual } from "crypto";
18087
19290
  import { Router as Router6 } from "express";
18088
19291
  var SUPPORTED_GITHUB_EVENTS = [
18089
19292
  "workflow_run",
@@ -18265,6 +19468,8 @@ function getLockPathForJob2(projectDir, jobId) {
18265
19468
  return qaLockPath(projectDir);
18266
19469
  case "audit":
18267
19470
  return auditLockPath(projectDir);
19471
+ case "ux":
19472
+ return uxLockPath(projectDir);
18268
19473
  case "slicer":
18269
19474
  case "planner":
18270
19475
  return plannerLockPath(projectDir);
@@ -18353,7 +19558,7 @@ function createJobRouteHandlers(ctx) {
18353
19558
  });
18354
19559
  return;
18355
19560
  }
18356
- const dispatchId = randomUUID();
19561
+ const dispatchId = randomUUID2();
18357
19562
  const child = spawn7("night-watch", [jobDef.cliCommand], {
18358
19563
  detached: true,
18359
19564
  stdio: "ignore",
@@ -18402,7 +19607,7 @@ function createProjectJobRoutes() {
18402
19607
 
18403
19608
  // ../server/dist/routes/log.routes.js
18404
19609
  init_dist();
18405
- import * as path38 from "path";
19610
+ import * as path40 from "path";
18406
19611
  import { Router as Router7 } from "express";
18407
19612
  function createLogRoutes(deps) {
18408
19613
  const { projectDir } = deps;
@@ -18421,7 +19626,7 @@ function createLogRoutes(deps) {
18421
19626
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
18422
19627
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
18423
19628
  const fileName = LOG_FILE_NAMES[name] || name;
18424
- const logPath = path38.join(projectDir, LOG_DIR, `${fileName}.log`);
19629
+ const logPath = path40.join(projectDir, LOG_DIR, `${fileName}.log`);
18425
19630
  const logLines = getLastLogLines(logPath, linesToRead);
18426
19631
  res.json({ name, lines: logLines });
18427
19632
  } catch (error2) {
@@ -18447,7 +19652,7 @@ function createProjectLogRoutes() {
18447
19652
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
18448
19653
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
18449
19654
  const fileName = LOG_FILE_NAMES[name] || name;
18450
- const logPath = path38.join(projectDir, LOG_DIR, `${fileName}.log`);
19655
+ const logPath = path40.join(projectDir, LOG_DIR, `${fileName}.log`);
18451
19656
  const logLines = getLastLogLines(logPath, linesToRead);
18452
19657
  res.json({ name, lines: logLines });
18453
19658
  } catch (error2) {
@@ -18482,7 +19687,7 @@ function createProjectPrdRoutes() {
18482
19687
 
18483
19688
  // ../server/dist/routes/roadmap.routes.js
18484
19689
  init_dist();
18485
- import * as path39 from "path";
19690
+ import * as path41 from "path";
18486
19691
  import { Router as Router9 } from "express";
18487
19692
  function createRoadmapRouteHandlers(ctx) {
18488
19693
  const router = Router9({ mergeParams: true });
@@ -18492,7 +19697,7 @@ function createRoadmapRouteHandlers(ctx) {
18492
19697
  const config = ctx.getConfig(req);
18493
19698
  const projectDir = ctx.getProjectDir(req);
18494
19699
  const status = getRoadmapStatus(projectDir, config);
18495
- const prdDir = path39.join(projectDir, config.prdDir);
19700
+ const prdDir = path41.join(projectDir, config.prdDir);
18496
19701
  const state = loadRoadmapState(prdDir);
18497
19702
  res.json({
18498
19703
  ...status,
@@ -18615,6 +19820,7 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
18615
19820
  const reviewerPlan = getSchedulingPlan(projectDir, config, "reviewer");
18616
19821
  const qaPlan = getSchedulingPlan(projectDir, config, "qa");
18617
19822
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
19823
+ const uxPlan = getSchedulingPlan(projectDir, config, "ux");
18618
19824
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
18619
19825
  const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
18620
19826
  const prResolverPlan = getSchedulingPlan(projectDir, config, "pr-resolver");
@@ -18624,6 +19830,7 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
18624
19830
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
18625
19831
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
18626
19832
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
19833
+ const uxInstalled = installed && config.ux.enabled && hasScheduledCommand(entries, "ux");
18627
19834
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
18628
19835
  const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
18629
19836
  const prResolverInstalled = installed && (config.prResolver?.enabled ?? true) && hasScheduledCommand(entries, "resolve");
@@ -18662,6 +19869,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
18662
19869
  manualDelayMinutes: auditPlan.manualDelayMinutes,
18663
19870
  balancedDelayMinutes: auditPlan.balancedDelayMinutes
18664
19871
  },
19872
+ ux: {
19873
+ schedule: config.ux.schedule,
19874
+ installed: uxInstalled,
19875
+ nextRun: uxInstalled ? addDelayToIsoString(computeNextRun(config.ux.schedule), uxPlan.totalDelayMinutes) : null,
19876
+ delayMinutes: uxPlan.totalDelayMinutes,
19877
+ manualDelayMinutes: uxPlan.manualDelayMinutes,
19878
+ balancedDelayMinutes: uxPlan.balancedDelayMinutes
19879
+ },
18665
19880
  planner: {
18666
19881
  schedule: config.roadmapScanner.slicerSchedule,
18667
19882
  installed: plannerInstalled,
@@ -18854,31 +20069,31 @@ function createQueueRoutes(deps) {
18854
20069
 
18855
20070
  // ../server/dist/index.js
18856
20071
  var __filename4 = fileURLToPath5(import.meta.url);
18857
- var __dirname4 = dirname12(__filename4);
20072
+ var __dirname4 = dirname13(__filename4);
18858
20073
  var JOB_RAW_BODY_LIMIT = "1mb";
18859
20074
  function setupJobRawBodyParsing(app) {
18860
20075
  app.use("/api/jobs", express.raw({ type: "*/*", limit: JOB_RAW_BODY_LIMIT }));
18861
20076
  app.use("/api/projects/:projectId/jobs", express.raw({ type: "*/*", limit: JOB_RAW_BODY_LIMIT }));
18862
20077
  }
18863
20078
  function resolveWebDistPath() {
18864
- const bundled = path40.join(__dirname4, "web");
18865
- if (fs38.existsSync(path40.join(bundled, "index.html")))
20079
+ const bundled = path42.join(__dirname4, "web");
20080
+ if (fs40.existsSync(path42.join(bundled, "index.html")))
18866
20081
  return bundled;
18867
20082
  let d = __dirname4;
18868
20083
  for (let i = 0; i < 8; i++) {
18869
- if (fs38.existsSync(path40.join(d, "turbo.json"))) {
18870
- const dev = path40.join(d, "web/dist");
18871
- if (fs38.existsSync(path40.join(dev, "index.html")))
20084
+ if (fs40.existsSync(path42.join(d, "turbo.json"))) {
20085
+ const dev = path42.join(d, "web/dist");
20086
+ if (fs40.existsSync(path42.join(dev, "index.html")))
18872
20087
  return dev;
18873
20088
  break;
18874
20089
  }
18875
- d = dirname12(d);
20090
+ d = dirname13(d);
18876
20091
  }
18877
20092
  return bundled;
18878
20093
  }
18879
20094
  function setupStaticFiles(app) {
18880
20095
  const webDistPath = resolveWebDistPath();
18881
- if (fs38.existsSync(webDistPath)) {
20096
+ if (fs40.existsSync(webDistPath)) {
18882
20097
  app.use(express.static(webDistPath));
18883
20098
  }
18884
20099
  app.use((req, res, next) => {
@@ -18886,8 +20101,8 @@ function setupStaticFiles(app) {
18886
20101
  next();
18887
20102
  return;
18888
20103
  }
18889
- const indexPath = path40.resolve(webDistPath, "index.html");
18890
- if (fs38.existsSync(indexPath)) {
20104
+ const indexPath = path42.resolve(webDistPath, "index.html");
20105
+ if (fs40.existsSync(indexPath)) {
18891
20106
  res.sendFile(indexPath, (err) => {
18892
20107
  if (err)
18893
20108
  next();
@@ -19026,7 +20241,7 @@ function createGlobalApp() {
19026
20241
  return app;
19027
20242
  }
19028
20243
  function bootContainer() {
19029
- initContainer(path40.dirname(getDbPath()));
20244
+ initContainer(path42.dirname(getDbPath()));
19030
20245
  }
19031
20246
  function startServer(projectDir, port) {
19032
20247
  bootContainer();
@@ -19041,7 +20256,7 @@ Night Watch UI http://localhost:${port}`);
19041
20256
  });
19042
20257
  setupGracefulShutdown(server);
19043
20258
  }
19044
- function startGlobalServer(port) {
20259
+ async function startGlobalServer(port) {
19045
20260
  bootContainer();
19046
20261
  const entries = loadRegistry();
19047
20262
  if (entries.length === 0) {
@@ -19052,12 +20267,14 @@ function startGlobalServer(port) {
19052
20267
  if (invalid.length > 0) {
19053
20268
  console.warn(`Warning: ${invalid.length} registered project(s) have invalid paths and will be skipped.`);
19054
20269
  }
20270
+ const summaries = await Promise.all(valid.map((entry) => collectProjectStartupSummary(entry)));
19055
20271
  console.log(`
19056
20272
  Night Watch Global UI`);
19057
20273
  console.log(`Managing ${valid.length} project(s):`);
19058
- for (const p of valid) {
19059
- console.log(` - ${p.name} (${p.path})`);
20274
+ for (const summary of summaries) {
20275
+ console.log(formatProjectStartupSummaryLine(summary));
19060
20276
  }
20277
+ console.log("");
19061
20278
  const app = createGlobalApp();
19062
20279
  const server = app.listen(port, () => {
19063
20280
  console.log(`Night Watch Global UI running at http://localhost:${port}`);
@@ -19079,8 +20296,8 @@ function isProcessRunning2(pid) {
19079
20296
  }
19080
20297
  function readPid(lockPath) {
19081
20298
  try {
19082
- if (!fs39.existsSync(lockPath)) return null;
19083
- const raw = fs39.readFileSync(lockPath, "utf-8").trim();
20299
+ if (!fs41.existsSync(lockPath)) return null;
20300
+ const raw = fs41.readFileSync(lockPath, "utf-8").trim();
19084
20301
  const pid = parseInt(raw, 10);
19085
20302
  return Number.isFinite(pid) ? pid : null;
19086
20303
  } catch {
@@ -19092,10 +20309,10 @@ function acquireServeLock(mode, port) {
19092
20309
  let stalePidCleaned;
19093
20310
  for (let attempt = 0; attempt < 2; attempt++) {
19094
20311
  try {
19095
- const fd = fs39.openSync(lockPath, "wx");
19096
- fs39.writeFileSync(fd, `${process.pid}
20312
+ const fd = fs41.openSync(lockPath, "wx");
20313
+ fs41.writeFileSync(fd, `${process.pid}
19097
20314
  `);
19098
- fs39.closeSync(fd);
20315
+ fs41.closeSync(fd);
19099
20316
  return { acquired: true, lockPath, stalePidCleaned };
19100
20317
  } catch (error2) {
19101
20318
  const err = error2;
@@ -19116,7 +20333,7 @@ function acquireServeLock(mode, port) {
19116
20333
  };
19117
20334
  }
19118
20335
  try {
19119
- fs39.unlinkSync(lockPath);
20336
+ fs41.unlinkSync(lockPath);
19120
20337
  if (existingPid) {
19121
20338
  stalePidCleaned = existingPid;
19122
20339
  }
@@ -19139,15 +20356,18 @@ function acquireServeLock(mode, port) {
19139
20356
  }
19140
20357
  function releaseServeLock(lockPath) {
19141
20358
  try {
19142
- if (!fs39.existsSync(lockPath)) return;
20359
+ if (!fs41.existsSync(lockPath)) return;
19143
20360
  const lockPid = readPid(lockPath);
19144
20361
  if (lockPid !== null && lockPid !== process.pid) return;
19145
- fs39.unlinkSync(lockPath);
20362
+ fs41.unlinkSync(lockPath);
19146
20363
  } catch {
19147
20364
  }
19148
20365
  }
20366
+ function isServeDebugEnabled() {
20367
+ return process.env.NIGHT_WATCH_DEBUG_SERVE === "1";
20368
+ }
19149
20369
  function serveCommand(program2) {
19150
- program2.command("serve").description("Start the Night Watch web UI server").option("-p, --port <number>", "Port to run the server on", "7575").option("-g, --global", "Start in global mode (manage all registered projects)").action((options) => {
20370
+ program2.command("serve").description("Start the Night Watch web UI server").option("-p, --port <number>", "Port to run the server on", "7575").option("-g, --global", "Start in global mode (manage all registered projects)").action(async (options) => {
19151
20371
  const port = parseInt(options.port, 10);
19152
20372
  if (isNaN(port) || port < 1 || port > 65535) {
19153
20373
  console.error(`Invalid port: ${options.port}. Port must be between 1 and 65535.`);
@@ -19171,11 +20391,14 @@ function serveCommand(program2) {
19171
20391
  `[serve] cleaned stale lock from PID ${lock.stalePidCleaned} (${lock.lockPath})`
19172
20392
  );
19173
20393
  }
19174
- console.log(`[serve] lock acquired ${lock.lockPath} pid=${process.pid}`);
20394
+ const debugServe = isServeDebugEnabled();
20395
+ if (debugServe) {
20396
+ console.log(`[serve] lock acquired ${lock.lockPath} pid=${process.pid}`);
20397
+ }
19175
20398
  process.on("exit", () => {
19176
20399
  releaseServeLock(lock.lockPath);
19177
20400
  });
19178
- if (options.global) {
20401
+ if (debugServe && options.global) {
19179
20402
  const execArgv = process.execArgv.length > 0 ? process.execArgv.join(" ") : "(none)";
19180
20403
  console.log(`[serve] mode=global port=${port} pid=${process.pid} node=${process.version}`);
19181
20404
  console.log(`[serve] execPath=${process.execPath}`);
@@ -19183,7 +20406,7 @@ function serveCommand(program2) {
19183
20406
  console.log(`[serve] argv=${process.argv.join(" ")}`);
19184
20407
  }
19185
20408
  if (options.global) {
19186
- startGlobalServer(port);
20409
+ await startGlobalServer(port);
19187
20410
  } else {
19188
20411
  const projectDir = process.cwd();
19189
20412
  startServer(projectDir, port);
@@ -19238,14 +20461,14 @@ function historyCommand(program2) {
19238
20461
  // src/commands/update.ts
19239
20462
  init_dist();
19240
20463
  import { spawnSync as spawnSync2 } from "child_process";
19241
- import * as fs40 from "fs";
19242
- import * as path41 from "path";
20464
+ import * as fs42 from "fs";
20465
+ import * as path43 from "path";
19243
20466
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
19244
20467
  function parseProjectDirs(projects, cwd) {
19245
20468
  if (!projects || projects.trim().length === 0) {
19246
20469
  return [cwd];
19247
20470
  }
19248
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path41.resolve(cwd, entry));
20471
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path43.resolve(cwd, entry));
19249
20472
  return Array.from(new Set(dirs));
19250
20473
  }
19251
20474
  function shouldInstallGlobal(options) {
@@ -19287,7 +20510,7 @@ function updateCommand(program2) {
19287
20510
  }
19288
20511
  const nightWatchBin = resolveNightWatchBin();
19289
20512
  for (const projectDir of projectDirs) {
19290
- if (!fs40.existsSync(projectDir) || !fs40.statSync(projectDir).isDirectory()) {
20513
+ if (!fs42.existsSync(projectDir) || !fs42.statSync(projectDir).isDirectory()) {
19291
20514
  warn(`Skipping invalid project directory: ${projectDir}`);
19292
20515
  continue;
19293
20516
  }
@@ -19331,8 +20554,8 @@ function prdStateCommand(program2) {
19331
20554
 
19332
20555
  // src/commands/retry.ts
19333
20556
  init_dist();
19334
- import * as fs41 from "fs";
19335
- import * as path42 from "path";
20557
+ import * as fs43 from "fs";
20558
+ import * as path44 from "path";
19336
20559
  function normalizePrdName(name) {
19337
20560
  if (!name.endsWith(".md")) {
19338
20561
  return `${name}.md`;
@@ -19340,26 +20563,26 @@ function normalizePrdName(name) {
19340
20563
  return name;
19341
20564
  }
19342
20565
  function getDonePrds(doneDir) {
19343
- if (!fs41.existsSync(doneDir)) {
20566
+ if (!fs43.existsSync(doneDir)) {
19344
20567
  return [];
19345
20568
  }
19346
- return fs41.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
20569
+ return fs43.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
19347
20570
  }
19348
20571
  function retryCommand(program2) {
19349
20572
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
19350
20573
  const projectDir = process.cwd();
19351
20574
  const config = loadConfig(projectDir);
19352
- const prdDir = path42.join(projectDir, config.prdDir);
19353
- const doneDir = path42.join(prdDir, "done");
20575
+ const prdDir = path44.join(projectDir, config.prdDir);
20576
+ const doneDir = path44.join(prdDir, "done");
19354
20577
  const normalizedPrdName = normalizePrdName(prdName);
19355
- const pendingPath = path42.join(prdDir, normalizedPrdName);
19356
- if (fs41.existsSync(pendingPath)) {
20578
+ const pendingPath = path44.join(prdDir, normalizedPrdName);
20579
+ if (fs43.existsSync(pendingPath)) {
19357
20580
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
19358
20581
  return;
19359
20582
  }
19360
- const donePath = path42.join(doneDir, normalizedPrdName);
19361
- if (fs41.existsSync(donePath)) {
19362
- fs41.renameSync(donePath, pendingPath);
20583
+ const donePath = path44.join(doneDir, normalizedPrdName);
20584
+ if (fs43.existsSync(donePath)) {
20585
+ fs43.renameSync(donePath, pendingPath);
19363
20586
  success(`Moved "${normalizedPrdName}" back to pending.`);
19364
20587
  dim(`From: ${donePath}`);
19365
20588
  dim(`To: ${pendingPath}`);
@@ -19611,7 +20834,7 @@ function prdsCommand(program2) {
19611
20834
 
19612
20835
  // src/commands/cancel.ts
19613
20836
  init_dist();
19614
- import * as fs42 from "fs";
20837
+ import * as fs44 from "fs";
19615
20838
  import * as readline2 from "readline";
19616
20839
  function getLockFilePaths2(projectDir) {
19617
20840
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -19658,7 +20881,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
19658
20881
  const pid = lockStatus.pid;
19659
20882
  if (!lockStatus.running) {
19660
20883
  try {
19661
- fs42.unlinkSync(lockPath);
20884
+ fs44.unlinkSync(lockPath);
19662
20885
  return {
19663
20886
  success: true,
19664
20887
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -19696,7 +20919,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
19696
20919
  await sleep2(3e3);
19697
20920
  if (!isProcessRunning3(pid)) {
19698
20921
  try {
19699
- fs42.unlinkSync(lockPath);
20922
+ fs44.unlinkSync(lockPath);
19700
20923
  } catch {
19701
20924
  }
19702
20925
  return {
@@ -19731,7 +20954,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
19731
20954
  await sleep2(500);
19732
20955
  if (!isProcessRunning3(pid)) {
19733
20956
  try {
19734
- fs42.unlinkSync(lockPath);
20957
+ fs44.unlinkSync(lockPath);
19735
20958
  } catch {
19736
20959
  }
19737
20960
  return {
@@ -19792,31 +21015,31 @@ function cancelCommand(program2) {
19792
21015
 
19793
21016
  // src/commands/slice.ts
19794
21017
  init_dist();
19795
- import * as fs43 from "fs";
19796
- import * as path43 from "path";
21018
+ import * as fs45 from "fs";
21019
+ import * as path45 from "path";
19797
21020
  function plannerLockPath2(projectDir) {
19798
21021
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
19799
21022
  }
19800
21023
  function acquirePlannerLock(projectDir) {
19801
21024
  const lockFile = plannerLockPath2(projectDir);
19802
- if (fs43.existsSync(lockFile)) {
19803
- const pidRaw = fs43.readFileSync(lockFile, "utf-8").trim();
21025
+ if (fs45.existsSync(lockFile)) {
21026
+ const pidRaw = fs45.readFileSync(lockFile, "utf-8").trim();
19804
21027
  const pid = parseInt(pidRaw, 10);
19805
21028
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
19806
21029
  return { acquired: false, lockFile, pid };
19807
21030
  }
19808
21031
  try {
19809
- fs43.unlinkSync(lockFile);
21032
+ fs45.unlinkSync(lockFile);
19810
21033
  } catch {
19811
21034
  }
19812
21035
  }
19813
- fs43.writeFileSync(lockFile, String(process.pid));
21036
+ fs45.writeFileSync(lockFile, String(process.pid));
19814
21037
  return { acquired: true, lockFile };
19815
21038
  }
19816
21039
  function releasePlannerLock(lockFile) {
19817
21040
  try {
19818
- if (fs43.existsSync(lockFile)) {
19819
- fs43.unlinkSync(lockFile);
21041
+ if (fs45.existsSync(lockFile)) {
21042
+ fs45.unlinkSync(lockFile);
19820
21043
  }
19821
21044
  } catch {
19822
21045
  }
@@ -19825,12 +21048,12 @@ function resolvePlannerIssueColumn(config) {
19825
21048
  return config.roadmapScanner.issueColumn === "Draft" ? "Draft" : "Ready";
19826
21049
  }
19827
21050
  function buildPlannerIssueBody(projectDir, config, result) {
19828
- const relativePrdPath = path43.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
19829
- const absolutePrdPath = path43.join(projectDir, config.prdDir, result.file ?? "");
21051
+ const relativePrdPath = path45.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
21052
+ const absolutePrdPath = path45.join(projectDir, config.prdDir, result.file ?? "");
19830
21053
  const sourceItem = result.item;
19831
21054
  let prdContent;
19832
21055
  try {
19833
- prdContent = fs43.readFileSync(absolutePrdPath, "utf-8");
21056
+ prdContent = fs45.readFileSync(absolutePrdPath, "utf-8");
19834
21057
  } catch {
19835
21058
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
19836
21059
  }
@@ -20060,7 +21283,7 @@ function sliceCommand(program2) {
20060
21283
  if (!options.dryRun && result.sliced) {
20061
21284
  await sendNotifications(config, {
20062
21285
  event: "run_succeeded",
20063
- projectName: path43.basename(projectDir),
21286
+ projectName: path45.basename(projectDir),
20064
21287
  exitCode,
20065
21288
  provider: config.provider,
20066
21289
  prTitle: result.item?.title
@@ -20068,7 +21291,7 @@ function sliceCommand(program2) {
20068
21291
  } else if (!options.dryRun && !nothingPending) {
20069
21292
  await sendNotifications(config, {
20070
21293
  event: "run_failed",
20071
- projectName: path43.basename(projectDir),
21294
+ projectName: path45.basename(projectDir),
20072
21295
  exitCode,
20073
21296
  provider: config.provider
20074
21297
  });
@@ -20100,21 +21323,21 @@ function sliceCommand(program2) {
20100
21323
 
20101
21324
  // src/commands/state.ts
20102
21325
  init_dist();
20103
- import * as os9 from "os";
20104
- import * as path44 from "path";
21326
+ import * as os11 from "os";
21327
+ import * as path46 from "path";
20105
21328
  import chalk5 from "chalk";
20106
21329
  import { Command } from "commander";
20107
21330
  function createStateCommand() {
20108
21331
  const state = new Command("state");
20109
21332
  state.description("Manage Night Watch state");
20110
21333
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
20111
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path44.join(os9.homedir(), GLOBAL_CONFIG_DIR);
21334
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path46.join(os11.homedir(), GLOBAL_CONFIG_DIR);
20112
21335
  if (opts.dryRun) {
20113
21336
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
20114
21337
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
20115
- console.log(` ${path44.join(nightWatchHome, "projects.json")}`);
20116
- console.log(` ${path44.join(nightWatchHome, "history.json")}`);
20117
- console.log(` ${path44.join(nightWatchHome, "prd-states.json")}`);
21338
+ console.log(` ${path46.join(nightWatchHome, "projects.json")}`);
21339
+ console.log(` ${path46.join(nightWatchHome, "history.json")}`);
21340
+ console.log(` ${path46.join(nightWatchHome, "prd-states.json")}`);
20118
21341
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
20119
21342
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
20120
21343
  return;
@@ -20152,8 +21375,8 @@ function createStateCommand() {
20152
21375
  init_dist();
20153
21376
  init_dist();
20154
21377
  import { execFileSync as execFileSync6 } from "child_process";
20155
- import * as fs44 from "fs";
20156
- import * as path45 from "path";
21378
+ import * as fs46 from "fs";
21379
+ import * as path47 from "path";
20157
21380
  import * as readline3 from "readline";
20158
21381
  import chalk6 from "chalk";
20159
21382
  async function run(fn) {
@@ -20175,7 +21398,7 @@ function getProvider(config, cwd) {
20175
21398
  return createBoardProvider(bp, cwd);
20176
21399
  }
20177
21400
  function defaultBoardTitle(cwd) {
20178
- return `${path45.basename(cwd)} Night Watch`;
21401
+ return `${path47.basename(cwd)} Night Watch`;
20179
21402
  }
20180
21403
  async function ensureBoardConfigured(config, cwd, provider, options) {
20181
21404
  if (config.boardProvider?.projectNumber) {
@@ -20376,11 +21599,11 @@ function boardCommand(program2) {
20376
21599
  let body = options.body ?? "";
20377
21600
  if (options.bodyFile) {
20378
21601
  const filePath = options.bodyFile;
20379
- if (!fs44.existsSync(filePath)) {
21602
+ if (!fs46.existsSync(filePath)) {
20380
21603
  console.error(`File not found: ${filePath}`);
20381
21604
  process.exit(1);
20382
21605
  }
20383
- body = fs44.readFileSync(filePath, "utf-8");
21606
+ body = fs46.readFileSync(filePath, "utf-8");
20384
21607
  }
20385
21608
  const labels = [];
20386
21609
  if (options.label) {
@@ -20621,12 +21844,12 @@ function boardCommand(program2) {
20621
21844
  const config = loadConfig(cwd);
20622
21845
  const provider = getProvider(config, cwd);
20623
21846
  await ensureBoardConfigured(config, cwd, provider);
20624
- const roadmapPath = options.roadmap ?? path45.join(cwd, "ROADMAP.md");
20625
- if (!fs44.existsSync(roadmapPath)) {
21847
+ const roadmapPath = options.roadmap ?? path47.join(cwd, "ROADMAP.md");
21848
+ if (!fs46.existsSync(roadmapPath)) {
20626
21849
  console.error(`Roadmap file not found: ${roadmapPath}`);
20627
21850
  process.exit(1);
20628
21851
  }
20629
- const roadmapContent = fs44.readFileSync(roadmapPath, "utf-8");
21852
+ const roadmapContent = fs46.readFileSync(roadmapPath, "utf-8");
20630
21853
  const items = parseRoadmap(roadmapContent);
20631
21854
  const uncheckedItems = getUncheckedItems(items);
20632
21855
  if (uncheckedItems.length === 0) {
@@ -20750,16 +21973,17 @@ function boardCommand(program2) {
20750
21973
  // src/commands/queue.ts
20751
21974
  init_dist();
20752
21975
  init_dist();
20753
- import * as path46 from "path";
21976
+ import * as path48 from "path";
20754
21977
  import { spawn as spawn8 } from "child_process";
20755
21978
  import chalk7 from "chalk";
20756
21979
  import { Command as Command2 } from "commander";
20757
- var logger6 = createLogger("queue");
21980
+ var logger7 = createLogger("queue");
20758
21981
  var VALID_JOB_TYPES2 = [
20759
21982
  "executor",
20760
21983
  "reviewer",
20761
21984
  "qa",
20762
21985
  "audit",
21986
+ "ux",
20763
21987
  "slicer",
20764
21988
  "planner",
20765
21989
  "pr-resolver",
@@ -20887,10 +22111,10 @@ function createQueueCommand() {
20887
22111
  process.exit(1);
20888
22112
  }
20889
22113
  }
20890
- const projectName = path46.basename(projectDir);
22114
+ const projectName = path48.basename(projectDir);
20891
22115
  const queueConfig = loadConfig(projectDir).queue;
20892
22116
  if (isJobPaused(projectDir, jobType)) {
20893
- logger6.info(`Skipping enqueue for paused job: ${jobType}`);
22117
+ logger7.info(`Skipping enqueue for paused job: ${jobType}`);
20894
22118
  return;
20895
22119
  }
20896
22120
  const id = enqueueJob(
@@ -20905,7 +22129,7 @@ function createQueueCommand() {
20905
22129
  });
20906
22130
  queue.command("resolve-key").description("Resolve the provider bucket key for a given project and job type").requiredOption("--project <dir>", "Project directory").requiredOption(
20907
22131
  "--job-type <type>",
20908
- "Job type (executor, reviewer, qa, audit, slicer, planner, pr-resolver, merger, manager)"
22132
+ "Job type (executor, reviewer, qa, audit, ux, slicer, planner, pr-resolver, merger, manager)"
20909
22133
  ).action((opts) => {
20910
22134
  try {
20911
22135
  const config = loadConfig(opts.project);
@@ -20927,18 +22151,19 @@ function createQueueCommand() {
20927
22151
  const configDir = _opts.projectDir ?? process.cwd();
20928
22152
  const entry = dispatchNextJob(loadConfig(configDir).queue);
20929
22153
  if (!entry) {
20930
- logger6.info("No pending jobs to dispatch");
22154
+ logger7.info("No pending jobs to dispatch");
20931
22155
  return;
20932
22156
  }
20933
22157
  if (isJobPaused(entry.projectPath, entry.jobType)) {
20934
- logger6.info(`Skipping paused queued job: ${entry.jobType} for ${entry.projectName}`);
22158
+ logger7.info(`Skipping paused queued job: ${entry.jobType} for ${entry.projectName}`);
20935
22159
  removeJob(entry.id);
20936
22160
  return;
20937
22161
  }
20938
- logger6.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
22162
+ logger7.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
20939
22163
  const scriptName = getScriptNameForJobType(entry.jobType);
20940
- if (!scriptName) {
20941
- logger6.error(`Unknown job type: ${entry.jobType}`);
22164
+ const jobDef = getJobDef(entry.jobType);
22165
+ if (!scriptName && (!jobDef || entry.jobType !== "ux")) {
22166
+ logger7.error(`Unknown job type: ${entry.jobType}`);
20942
22167
  return;
20943
22168
  }
20944
22169
  let projectEnv;
@@ -20956,21 +22181,22 @@ function createQueueCommand() {
20956
22181
  NW_QUEUE_DISPATCHED: "1",
20957
22182
  NW_QUEUE_ENTRY_ID: String(entry.id)
20958
22183
  };
20959
- const scriptPath = getScriptPath(scriptName);
20960
- logger6.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
20961
22184
  try {
20962
- const child = spawn8("bash", [scriptPath, entry.projectPath], {
22185
+ const command = scriptName ? "bash" : "night-watch";
22186
+ const args = scriptName ? [getScriptPath(scriptName), entry.projectPath] : [jobDef.cliCommand];
22187
+ logger7.info(`Spawning: ${command} ${args.join(" ")}`);
22188
+ const child = spawn8(command, args, {
20963
22189
  detached: true,
20964
22190
  stdio: "ignore",
20965
22191
  env,
20966
22192
  cwd: entry.projectPath
20967
22193
  });
20968
22194
  child.unref();
20969
- logger6.info(`Spawned PID: ${child.pid}`);
22195
+ logger7.info(`Spawned PID: ${child.pid}`);
20970
22196
  markJobRunning(entry.id, child.pid ?? void 0);
20971
22197
  } catch (error2) {
20972
22198
  updateJobStatus(entry.id, "pending");
20973
- logger6.error(
22199
+ logger7.error(
20974
22200
  `Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
20975
22201
  );
20976
22202
  process.exit(1);
@@ -20988,7 +22214,7 @@ function createQueueCommand() {
20988
22214
  if (isJobPaused(projectDir, jobType)) {
20989
22215
  process.exit(2);
20990
22216
  }
20991
- const projectName = path46.basename(projectDir);
22217
+ const projectName = path48.basename(projectDir);
20992
22218
  const callerPid = opts.pid ? parseInt(opts.pid, 10) : void 0;
20993
22219
  const result = claimJobSlot(
20994
22220
  projectDir,
@@ -21124,7 +22350,7 @@ function notifyCommand(program2) {
21124
22350
 
21125
22351
  // src/commands/summary.ts
21126
22352
  init_dist();
21127
- import path47 from "path";
22353
+ import path49 from "path";
21128
22354
  import chalk8 from "chalk";
21129
22355
  function formatDuration2(seconds) {
21130
22356
  if (seconds === null) return "-";
@@ -21154,7 +22380,7 @@ function formatJobStatus(status) {
21154
22380
  return chalk8.dim(status);
21155
22381
  }
21156
22382
  function getProjectName2(projectPath) {
21157
- return path47.basename(projectPath) || projectPath;
22383
+ return path49.basename(projectPath) || projectPath;
21158
22384
  }
21159
22385
  function formatProvider(providerKey) {
21160
22386
  return providerKey.split(":")[0] || providerKey;
@@ -21273,7 +22499,7 @@ function summaryCommand(program2) {
21273
22499
  // src/commands/resolve.ts
21274
22500
  init_dist();
21275
22501
  import { execFileSync as execFileSync7 } from "child_process";
21276
- import * as path48 from "path";
22502
+ import * as path50 from "path";
21277
22503
  function buildEnvVars6(config, options) {
21278
22504
  const env = buildBaseEnvVars(config, "pr-resolver", options.dryRun);
21279
22505
  env.NW_PR_RESOLVER_MAX_RUNTIME = String(config.prResolver.maxRuntime);
@@ -21426,7 +22652,7 @@ ${stderr}`);
21426
22652
  }
21427
22653
  await sendNotifications(config, {
21428
22654
  event: notificationEvent,
21429
- projectName: path48.basename(projectDir),
22655
+ projectName: path50.basename(projectDir),
21430
22656
  exitCode,
21431
22657
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
21432
22658
  });
@@ -21441,7 +22667,7 @@ ${stderr}`);
21441
22667
 
21442
22668
  // src/commands/merge.ts
21443
22669
  init_dist();
21444
- import * as path49 from "path";
22670
+ import * as path51 from "path";
21445
22671
  function buildEnvVars7(config, options) {
21446
22672
  const env = buildBaseEnvVars(config, "merger", options.dryRun);
21447
22673
  env.NW_MERGER_MAX_RUNTIME = String(config.merger.maxRuntime);
@@ -21476,7 +22702,7 @@ function resolveMergeNotificationEvent(exitCode, mergedCount, failedCount) {
21476
22702
  }
21477
22703
  return null;
21478
22704
  }
21479
- function printDryRun(config, envVars, scriptPath, projectDir) {
22705
+ function printDryRun2(config, envVars, scriptPath, projectDir) {
21480
22706
  header("Dry Run: Merge Orchestrator");
21481
22707
  const mergerProvider = resolveJobProvider(config, "merger");
21482
22708
  header("Configuration");
@@ -21513,20 +22739,26 @@ function mergeCommand(program2) {
21513
22739
  const projectDir = process.cwd();
21514
22740
  let config = loadConfig(projectDir);
21515
22741
  config = applyCliOverrides6(config, options);
22742
+ const commandStartedAt = Date.now();
22743
+ await trackCommandStarted("merge", config);
21516
22744
  if (!config.merger.enabled && !options.dryRun) {
21517
22745
  info("Merge orchestrator is disabled in config; skipping.");
22746
+ await trackCommandCompleted("merge", commandStartedAt, 0, config);
21518
22747
  process.exit(0);
21519
22748
  }
21520
22749
  const envVars = buildEnvVars7(config, options);
21521
22750
  const scriptPath = getScriptPath("night-watch-merger-cron.sh");
21522
22751
  if (options.dryRun) {
21523
- printDryRun(config, envVars, scriptPath, projectDir);
22752
+ printDryRun2(config, envVars, scriptPath, projectDir);
22753
+ await trackCommandCompleted("merge", commandStartedAt, 0, config);
21524
22754
  process.exit(0);
21525
22755
  }
21526
22756
  const spinner = createSpinner("Running merge orchestrator...");
21527
22757
  spinner.start();
21528
22758
  try {
21529
22759
  const startedAt = Date.now();
22760
+ const provider = envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "merger");
22761
+ await trackJobStarted("merger", provider, config);
21530
22762
  await maybeApplyCronSchedulingDelay(config, "merger", projectDir);
21531
22763
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
21532
22764
  scriptPath,
@@ -21536,6 +22768,14 @@ function mergeCommand(program2) {
21536
22768
  const finishedAt = Date.now();
21537
22769
  const scriptResult = parseScriptResult(`${stdout}
21538
22770
  ${stderr}`);
22771
+ await trackJobCompletedOrFailed(
22772
+ "merger",
22773
+ provider,
22774
+ startedAt,
22775
+ exitCode,
22776
+ config,
22777
+ scriptResult?.status
22778
+ );
21539
22779
  if (exitCode === 0) {
21540
22780
  if (scriptResult?.status === "queued") {
21541
22781
  spinner.succeed("Merge orchestrator queued \u2014 another job is currently running");
@@ -21550,6 +22790,14 @@ ${stderr}`);
21550
22790
  const mergedCount = parseInt(scriptResult?.data?.merged ?? "0", 10);
21551
22791
  const failedCount = parseInt(scriptResult?.data?.failed ?? "0", 10);
21552
22792
  const notificationEvent = resolveMergeNotificationEvent(exitCode, mergedCount, failedCount);
22793
+ if (mergedCount > 0 && exitCode === 0) {
22794
+ fireTelemetryEvent("auto_merge_completed", {
22795
+ ...buildTelemetryBaseProperties(config),
22796
+ jobType: "merger",
22797
+ provider,
22798
+ success: true
22799
+ });
22800
+ }
21553
22801
  if (!options.dryRun) {
21554
22802
  try {
21555
22803
  recordJobOutcome({
@@ -21577,15 +22825,19 @@ ${stderr}`);
21577
22825
  if (notificationEvent) {
21578
22826
  await sendNotifications(config, {
21579
22827
  event: notificationEvent,
21580
- projectName: path49.basename(projectDir),
22828
+ projectName: path51.basename(projectDir),
21581
22829
  exitCode,
21582
22830
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
21583
22831
  });
21584
22832
  }
22833
+ await trackCommandCompleted("merge", commandStartedAt, exitCode, config);
21585
22834
  process.exit(exitCode);
21586
22835
  } catch (err) {
21587
22836
  spinner.fail("Failed to execute merge command");
21588
22837
  error(`${err instanceof Error ? err.message : String(err)}`);
22838
+ await trackCommandCompleted("merge", commandStartedAt, 1, config, {
22839
+ errorCategory: err instanceof Error ? err.message : String(err)
22840
+ });
21589
22841
  process.exit(1);
21590
22842
  }
21591
22843
  });
@@ -21593,7 +22845,7 @@ ${stderr}`);
21593
22845
 
21594
22846
  // src/commands/manager.ts
21595
22847
  init_dist();
21596
- import * as path50 from "path";
22848
+ import * as path52 from "path";
21597
22849
  function resolveRunManager() {
21598
22850
  const runManager2 = runManager;
21599
22851
  if (typeof runManager2 !== "function") {
@@ -21603,17 +22855,17 @@ function resolveRunManager() {
21603
22855
  }
21604
22856
  return runManager2;
21605
22857
  }
21606
- function writeJson(value) {
22858
+ function writeJson2(value) {
21607
22859
  process.stdout.write(`${JSON.stringify(value, null, 2)}
21608
22860
  `);
21609
22861
  }
21610
- function parseTimeout(timeout) {
22862
+ function parseTimeout2(timeout) {
21611
22863
  if (!timeout) return void 0;
21612
22864
  const parsed = parseInt(timeout, 10);
21613
22865
  return Number.isNaN(parsed) || parsed < 0 ? void 0 : parsed;
21614
22866
  }
21615
22867
  function buildManagerRunOptions(options) {
21616
- const timeout = parseTimeout(options.timeout);
22868
+ const timeout = parseTimeout2(options.timeout);
21617
22869
  return {
21618
22870
  dryRun: options.dryRun === true,
21619
22871
  ...timeout !== void 0 ? { timeout } : {},
@@ -21621,7 +22873,7 @@ function buildManagerRunOptions(options) {
21621
22873
  };
21622
22874
  }
21623
22875
  function applyManagerCliOverrides(config, options) {
21624
- const timeout = parseTimeout(options.timeout);
22876
+ const timeout = parseTimeout2(options.timeout);
21625
22877
  let overridden = config;
21626
22878
  if (timeout !== void 0) {
21627
22879
  overridden = {
@@ -21673,7 +22925,7 @@ async function sendManagerNotifications(config, projectDir, result) {
21673
22925
  if (!item.shouldNotify || !item.event) continue;
21674
22926
  await sendNotifications(config, {
21675
22927
  event: item.event,
21676
- projectName: path50.basename(projectDir),
22928
+ projectName: path52.basename(projectDir),
21677
22929
  provider: resolveJobProvider(config, "manager"),
21678
22930
  exitCode: 0,
21679
22931
  failureReason: item.title,
@@ -21708,7 +22960,7 @@ function managerCommand(program2) {
21708
22960
  const runOptions = buildManagerRunOptions(options);
21709
22961
  if (!managerConfig.enabled && !runOptions.dryRun) {
21710
22962
  if (options.json) {
21711
- writeJson({ dryRun: false, skipped: true, reason: "manager-disabled" });
22963
+ writeJson2({ dryRun: false, skipped: true, reason: "manager-disabled" });
21712
22964
  } else {
21713
22965
  info("Manager is disabled in config; skipping run.");
21714
22966
  }
@@ -21746,7 +22998,7 @@ function managerCommand(program2) {
21746
22998
  }
21747
22999
  }
21748
23000
  if (options.json) {
21749
- writeJson(buildJsonResult(result, runOptions));
23001
+ writeJson2(buildJsonResult(result, runOptions));
21750
23002
  } else {
21751
23003
  if (exitCode === 0) {
21752
23004
  spinner?.succeed("Manager completed successfully");
@@ -21787,12 +23039,55 @@ function managerCommand(program2) {
21787
23039
  });
21788
23040
  }
21789
23041
 
23042
+ // src/commands/telemetry.ts
23043
+ init_dist();
23044
+ function formatInstallId(installId) {
23045
+ return installId.slice(0, 8);
23046
+ }
23047
+ function printStatus() {
23048
+ const state = getTelemetryEffectiveState();
23049
+ const status = state.enabled ? "enabled" : "disabled";
23050
+ console.log(`Telemetry: ${status}`);
23051
+ console.log(`Reason: ${state.reason}`);
23052
+ console.log(`Config path: ${state.path}`);
23053
+ console.log(`Install ID: ${formatInstallId(state.config.installId)}`);
23054
+ console.log("Privacy docs: docs/privacy.md");
23055
+ console.log("Disable: night-watch telemetry disable");
23056
+ console.log("Env opt-outs: NW_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1");
23057
+ }
23058
+ function telemetryCommand(program2) {
23059
+ const command = program2.command("telemetry").description("Manage anonymous product telemetry");
23060
+ command.command("status").description("Show telemetry status and opt-out information").action(() => {
23061
+ printStatus();
23062
+ });
23063
+ command.command("disable").description("Disable anonymous product telemetry").action(async () => {
23064
+ const state = getTelemetryEffectiveState();
23065
+ if (state.enabled) {
23066
+ fireTelemetryEvent("telemetry_disabled");
23067
+ }
23068
+ setTelemetryEnabled(false);
23069
+ console.log("Telemetry disabled.");
23070
+ console.log("You can also set NW_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1.");
23071
+ });
23072
+ command.command("enable").description("Enable anonymous product telemetry").action(() => {
23073
+ setTelemetryEnabled(true);
23074
+ const envDisabled = isTelemetryEnvDisabled();
23075
+ if (!envDisabled) {
23076
+ fireTelemetryEvent("telemetry_enabled");
23077
+ }
23078
+ console.log("Telemetry enabled.");
23079
+ if (envDisabled) {
23080
+ console.log(`Currently overridden by ${envDisabled}.`);
23081
+ }
23082
+ });
23083
+ }
23084
+
21790
23085
  // src/commands/agent.ts
21791
23086
  init_dist();
21792
23087
  var SCHEMA_VERSION2 = 1;
21793
23088
  var JSON_OPTION = "--json";
21794
23089
  var JSON_OPTION_DESCRIPTION = "Output as JSON";
21795
- function writeJson2(value) {
23090
+ function writeJson3(value) {
21796
23091
  process.stdout.write(`${JSON.stringify(value, null, 2)}
21797
23092
  `);
21798
23093
  }
@@ -21949,7 +23244,7 @@ function normalizeJobType(job) {
21949
23244
  function agentCommand(program2) {
21950
23245
  const agent = program2.command("agent").description("Machine-readable agent operations");
21951
23246
  agent.command("status").description("Print a stable machine-readable project snapshot").requiredOption(JSON_OPTION, "Output status as JSON").action(async () => {
21952
- writeJson2(await buildAgentStatus(process.cwd()));
23247
+ writeJson3(await buildAgentStatus(process.cwd()));
21953
23248
  });
21954
23249
  }
21955
23250
  function configCommand(program2) {
@@ -21957,18 +23252,18 @@ function configCommand(program2) {
21957
23252
  config.command("list").description("Print resolved config").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((options) => {
21958
23253
  const value = loadConfig(process.cwd());
21959
23254
  if (options.json) {
21960
- writeJson2({ schemaVersion: SCHEMA_VERSION2, config: value });
23255
+ writeJson3({ schemaVersion: SCHEMA_VERSION2, config: value });
21961
23256
  } else {
21962
- writeJson2(value);
23257
+ writeJson3(value);
21963
23258
  }
21964
23259
  });
21965
23260
  config.command("get <path>").description("Read a resolved config value by dot path").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((dotPath, options) => {
21966
23261
  try {
21967
23262
  const result = getConfigValue(process.cwd(), dotPath);
21968
23263
  if (options.json) {
21969
- writeJson2({ schemaVersion: SCHEMA_VERSION2, ...result });
23264
+ writeJson3({ schemaVersion: SCHEMA_VERSION2, ...result });
21970
23265
  } else {
21971
- writeJson2(result.value);
23266
+ writeJson3(result.value);
21972
23267
  }
21973
23268
  } catch (error2) {
21974
23269
  fail(error2 instanceof Error ? error2.message : String(error2), options);
@@ -21978,7 +23273,7 @@ function configCommand(program2) {
21978
23273
  try {
21979
23274
  const result = setConfigValue(process.cwd(), dotPath, parseConfigValue(rawValue));
21980
23275
  if (options.json) {
21981
- writeJson2({ schemaVersion: SCHEMA_VERSION2, ok: true, ...result });
23276
+ writeJson3({ schemaVersion: SCHEMA_VERSION2, ok: true, ...result });
21982
23277
  } else {
21983
23278
  process.stdout.write(`Updated ${result.path}
21984
23279
  `);
@@ -21994,7 +23289,7 @@ function healthCommand(program2) {
21994
23289
  const snapshot = await fetchStatusSnapshot(process.cwd(), config);
21995
23290
  const health = buildHealth(snapshot, config);
21996
23291
  if (options.json) {
21997
- writeJson2(health);
23292
+ writeJson3(health);
21998
23293
  } else {
21999
23294
  for (const check of health.checks) {
22000
23295
  process.stdout.write(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}
@@ -22013,7 +23308,7 @@ function jobCommand(program2) {
22013
23308
  const jobType = normalizeJobType(jobName);
22014
23309
  const result = setConfigValue(process.cwd(), `pausedJobs.${jobType}`, true);
22015
23310
  if (options.json) {
22016
- writeJson2({
23311
+ writeJson3({
22017
23312
  schemaVersion: SCHEMA_VERSION2,
22018
23313
  ok: true,
22019
23314
  job: jobType,
@@ -22032,7 +23327,7 @@ function jobCommand(program2) {
22032
23327
  const jobType = normalizeJobType(jobName);
22033
23328
  const result = setConfigValue(process.cwd(), `pausedJobs.${jobType}`, false);
22034
23329
  if (options.json) {
22035
- writeJson2({
23330
+ writeJson3({
22036
23331
  schemaVersion: SCHEMA_VERSION2,
22037
23332
  ok: true,
22038
23333
  job: jobType,
@@ -22057,26 +23352,58 @@ function jobCommand(program2) {
22057
23352
  });
22058
23353
  }
22059
23354
 
23355
+ // src/telemetry-bootstrap.ts
23356
+ init_dist();
23357
+ var NOTICE_LINES = [
23358
+ "Night Watch collects anonymous product telemetry to understand usage and improve the CLI.",
23359
+ "Telemetry is enabled by default.",
23360
+ "Disable anytime with `night-watch telemetry disable`, `NW_TELEMETRY_DISABLED=1`, or `DO_NOT_TRACK=1`.",
23361
+ "Collected: CLI version, command/job type, provider, success/failure, duration, exit code, platform, Node major version, board mode, registered project count, and error category.",
23362
+ "Never collected: repo names, paths, remotes, branches, PR/issue titles/bodies/URLs/numbers, prompts, provider output, diffs, file paths, usernames/emails, hostnames, env vars, tokens/secrets, or raw stack traces.",
23363
+ "Privacy details: docs/privacy.md"
23364
+ ];
23365
+ function shouldSuppressNotice(argv) {
23366
+ return argv.includes("--json") || argv.includes("-h") || argv.includes("--help");
23367
+ }
23368
+ function bootstrapTelemetry(cliVersion2, argv = process.argv) {
23369
+ setCliTelemetryVersion(cliVersion2);
23370
+ if (isTelemetryEnvDisabled() || shouldSuppressNotice(argv)) {
23371
+ return;
23372
+ }
23373
+ try {
23374
+ const state = getTelemetryEffectiveState();
23375
+ if (!state.enabled || state.config.noticeShownAt) {
23376
+ return;
23377
+ }
23378
+ console.error(NOTICE_LINES.join("\n"));
23379
+ markTelemetryNoticeShown();
23380
+ fireTelemetryEvent("cli_first_run");
23381
+ } catch {
23382
+ }
23383
+ }
23384
+
22060
23385
  // src/cli.ts
22061
23386
  var __filename5 = fileURLToPath6(import.meta.url);
22062
- var __dirname5 = dirname13(__filename5);
23387
+ var __dirname5 = dirname14(__filename5);
22063
23388
  function findPackageRoot(dir) {
22064
23389
  let d = dir;
22065
23390
  for (let i = 0; i < 5; i++) {
22066
- if (existsSync36(join39(d, "package.json"))) return d;
22067
- d = dirname13(d);
23391
+ if (existsSync37(join41(d, "package.json"))) return d;
23392
+ d = dirname14(d);
22068
23393
  }
22069
23394
  return dir;
22070
23395
  }
22071
23396
  var packageRoot = findPackageRoot(__dirname5);
22072
- var packageJson = JSON.parse(readFileSync23(join39(packageRoot, "package.json"), "utf-8"));
23397
+ var packageJson = JSON.parse(readFileSync24(join41(packageRoot, "package.json"), "utf-8"));
22073
23398
  var program = new Command3();
22074
23399
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
23400
+ bootstrapTelemetry(packageJson.version);
22075
23401
  initCommand(program);
22076
23402
  runCommand(program);
22077
23403
  reviewCommand(program);
22078
23404
  qaCommand(program);
22079
23405
  auditCommand(program);
23406
+ uxCommand(program);
22080
23407
  analyticsCommand(program);
22081
23408
  installCommand(program);
22082
23409
  uninstallCommand(program);
@@ -22102,6 +23429,7 @@ summaryCommand(program);
22102
23429
  resolveCommand(program);
22103
23430
  mergeCommand(program);
22104
23431
  managerCommand(program);
23432
+ telemetryCommand(program);
22105
23433
  agentCommand(program);
22106
23434
  configCommand(program);
22107
23435
  healthCommand(program);