@analyticscli/growth-engineer 0.1.0-preview.1 → 0.1.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,17 @@ import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
11
11
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
12
12
  const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
13
  const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
14
+ const DEFAULT_GROWTH_INTERVAL_MINUTES = 1440;
15
+ const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
14
16
  const CONNECTOR_KEYS = ['analytics', 'github', 'revenuecat', 'sentry', 'asc'];
17
+ class WizardAbortError extends Error {
18
+ exitCode;
19
+ constructor(message, exitCode = 130) {
20
+ super(message);
21
+ this.name = 'WizardAbortError';
22
+ this.exitCode = exitCode;
23
+ }
24
+ }
15
25
  const CONNECTOR_DEFINITIONS = [
16
26
  {
17
27
  key: 'analytics',
@@ -47,33 +57,33 @@ const CONNECTOR_DEFINITIONS = [
47
57
  const DEFAULT_CADENCE_PLAN = [
48
58
  {
49
59
  key: 'daily',
50
- title: 'Daily production guardrail',
60
+ title: 'Daily Sentry and production guardrail',
51
61
  intervalDays: 1,
52
62
  criticalOnly: true,
53
- focusAreas: ['crash', 'conversion', 'paywall'],
54
- sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'asc_cli', 'revenuecat'],
55
- objective: 'Only investigate critical production blockers and business anomalies: Sentry/GlitchTip production errors, crashes, very low users, conversion, purchases, or other urgent drops.',
56
- instructions: 'Do exact root-cause analysis with connected production data, memory/state, release context, and recent code changes. Produce the fix or next debugging step; avoid generic growth ideas.',
63
+ focusAreas: ['sentry_errors', 'crash', 'onboarding', 'conversion', 'paywall', 'purchase'],
64
+ sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'revenuecat', 'asc_cli', 'feedback', 'github'],
65
+ objective: 'Analyze every configured project for critical production blockers: Sentry/GlitchTip errors, crashes, onboarding or purchase drop-offs, zero-conversion days, missing buyers, very low users, and other silent business anomalies.',
66
+ instructions: 'Compare against recent baselines across connected sources and code changes. If the finding is critical, produce the exact fix or next debugging step and prefer a GitHub issue or draft PR when GitHub write access is configured; otherwise hand off via OpenClaw chat. Avoid generic growth ideas.',
57
67
  },
58
68
  {
59
69
  key: 'weekly',
60
- title: 'Weekly conversion, traffic, and RevenueCat review',
70
+ title: 'Weekly executive product and growth summary',
61
71
  intervalDays: 7,
62
72
  criticalOnly: false,
63
- focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention'],
64
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
65
- objective: 'Analyze total conversion, traffic quality, activation, retention, RevenueCat trials/subscriptions/revenue/churn, source mix, reviews, releases, and stability.',
66
- instructions: 'Pick one to three high-confidence growth bets with evidence, expected KPI movement, likely code/store surfaces, and verification plan.',
73
+ focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention', 'stability'],
74
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
75
+ objective: 'Create an executive summary across all configured projects, connectors, recent releases, code changes, revenue, activation, retention, reviews, and production stability.',
76
+ instructions: 'Pick one to three high-confidence improvements with evidence, expected KPI movement, likely code/store surfaces, owner-ready next steps, and a verification plan. Create GitHub issues or draft PR proposals only when the evidence is specific enough.',
67
77
  },
68
78
  {
69
79
  key: 'monthly',
70
- title: 'Monthly business and product review',
80
+ title: 'Monthly deep product, business, and code review',
71
81
  intervalDays: 30,
72
82
  criticalOnly: false,
73
- focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding'],
74
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
75
- objective: 'Compare MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, and crash totals month-over-month.',
76
- instructions: 'Decide what should be built, changed, or deleted next and explain why it should move revenue, activation, retention, or acquisition quality.',
83
+ focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding', 'codebase'],
84
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
85
+ objective: 'Compare all configured projects month-over-month: MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, crash totals, and codebase changes.',
86
+ instructions: 'Decide what should be built, changed, deleted, or instrumented next. Tie conclusions to connector data plus codebase evidence and explain why each recommendation should move revenue, activation, retention, stability, or acquisition quality.',
77
87
  },
78
88
  {
79
89
  key: 'quarterly',
@@ -81,8 +91,8 @@ const DEFAULT_CADENCE_PLAN = [
81
91
  intervalDays: 91,
82
92
  criticalOnly: false,
83
93
  focusAreas: ['marketing', 'paywall', 'retention', 'conversion', 'onboarding'],
84
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback'],
85
- objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, and major funnel bets.',
94
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'github', 'sentry'],
95
+ objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, codebase constraints, and major funnel bets across every configured project.',
86
96
  instructions: 'Find structural constraints and durable opportunities. Tie recommendations to cohort behavior, monetization, reviews, channel quality, and shipped changes.',
87
97
  },
88
98
  {
@@ -92,7 +102,7 @@ const DEFAULT_CADENCE_PLAN = [
92
102
  criticalOnly: false,
93
103
  focusAreas: ['retention', 'conversion', 'paywall', 'marketing', 'general'],
94
104
  sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
95
- objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether strategy still matches the best users.',
105
+ objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether product/code strategy still matches the best users across configured projects.',
96
106
  instructions: 'Prioritize measurement fixes and system changes that make future analysis more trustworthy. Identify stale events, missing attribution, weak identity, and misleading dashboards.',
97
107
  },
98
108
  {
@@ -102,7 +112,7 @@ const DEFAULT_CADENCE_PLAN = [
102
112
  criticalOnly: false,
103
113
  focusAreas: ['marketing', 'retention', 'paywall', 'conversion', 'general'],
104
114
  sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
105
- objective: 'Reset strategy from evidence: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
115
+ objective: 'Reset strategy from evidence across every configured project: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
106
116
  instructions: 'Use the full year of memory, releases, revenue, acquisition, reviews, code changes, and cohort behavior. Produce strategic experiments and stop-doing decisions.',
107
117
  },
108
118
  ];
@@ -268,19 +278,24 @@ function withMissingRequiredAnalyticsConnector(selected) {
268
278
  return orderConnectors(selected);
269
279
  return orderConnectors(['analytics', ...selected]);
270
280
  }
271
- async function askConnectorSelection(rl) {
272
- return askConnectorSelectionWithHealth(rl, {}, []);
273
- }
274
281
  async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = []) {
275
282
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
276
283
  return await askConnectorSelectionByText(rl, healthByConnector);
277
284
  }
278
285
  rl.pause();
286
+ let completed = false;
279
287
  try {
280
- return await askConnectorSelectionByKeys(healthByConnector, initialSelected);
288
+ const selected = await askConnectorSelectionByKeys(healthByConnector, initialSelected);
289
+ completed = true;
290
+ return selected;
281
291
  }
282
292
  finally {
283
- rl.resume();
293
+ if (completed) {
294
+ rl.resume();
295
+ }
296
+ else {
297
+ process.stdin.pause();
298
+ }
284
299
  }
285
300
  }
286
301
  async function askConnectorSelectionByText(rl, healthByConnector = {}) {
@@ -326,22 +341,107 @@ function printConnectorIntro() {
326
341
  process.stdout.write(`\n${ANSI.bold}OpenClaw connector setup${ANSI.reset}\n`);
327
342
  process.stdout.write(`${ANSI.dim}Secrets stay local on this host. Do not paste them into any chat or social channel.${ANSI.reset}\n\n`);
328
343
  }
329
- async function withTerminalLoading(message, task) {
330
- const frames = ['-', '\\', '|', '/'];
331
- let index = 0;
332
- process.stdout.write(`${message} ${frames[index]}`);
333
- const timer = setInterval(() => {
334
- index = (index + 1) % frames.length;
335
- process.stdout.write(`\r${message} ${frames[index]}`);
336
- }, 120);
344
+ async function askMenuChoice(rl, { title, subtitle = 'Use Up/Down to move, Enter to continue.', options, defaultValue, renderHeader, }) {
345
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
346
+ process.stdout.write(`\n${title}\n`);
347
+ options.forEach((option, index) => {
348
+ process.stdout.write(` ${index + 1}) ${option.label}: ${option.detail}\n`);
349
+ });
350
+ const defaultIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
351
+ const answer = await ask(rl, `Setup area (1-${options.length})`, String(defaultIndex + 1));
352
+ const selected = options[Number(answer.trim()) - 1] || options[defaultIndex];
353
+ return selected.value;
354
+ }
355
+ rl.pause();
356
+ let completed = false;
337
357
  try {
338
- return await task;
358
+ const selected = await askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader });
359
+ completed = true;
360
+ return selected;
339
361
  }
340
362
  finally {
341
- clearInterval(timer);
342
- process.stdout.write(`\r${message} done\n`);
363
+ if (completed) {
364
+ rl.resume();
365
+ }
366
+ else {
367
+ process.stdin.pause();
368
+ }
343
369
  }
344
370
  }
371
+ async function askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader, }) {
372
+ emitKeypressEvents(process.stdin);
373
+ const wasRaw = process.stdin.isRaw;
374
+ const wasPaused = process.stdin.isPaused();
375
+ process.stdin.setRawMode(true);
376
+ process.stdin.resume();
377
+ let cursorIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
378
+ return await new Promise((resolve, reject) => {
379
+ const cleanup = () => {
380
+ process.stdin.off('keypress', onKeypress);
381
+ process.stdin.setRawMode(Boolean(wasRaw));
382
+ if (wasPaused) {
383
+ process.stdin.pause();
384
+ }
385
+ process.stdout.write(ANSI.showCursor);
386
+ };
387
+ const render = () => {
388
+ process.stdout.write('\x1b[2J\x1b[H');
389
+ renderHeader?.();
390
+ process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
391
+ process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
392
+ for (let index = 0; index < options.length; index += 1) {
393
+ const option = options[index];
394
+ const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
395
+ const number = `${index + 1})`;
396
+ process.stdout.write(`${pointer} ${number} ${ANSI.bold}${option.label}${ANSI.reset}\n`);
397
+ writeWrapped(option.detail, ' ', ANSI.dim);
398
+ }
399
+ process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Number keys 1-${options.length} select directly.${ANSI.reset}\n`);
400
+ };
401
+ const cancel = () => {
402
+ cleanup();
403
+ process.stdout.write('\n');
404
+ reject(new WizardAbortError('Setup cancelled.'));
405
+ };
406
+ const finish = () => {
407
+ cleanup();
408
+ process.stdout.write('\x1b[2J\x1b[H');
409
+ resolve(options[cursorIndex]?.value || defaultValue);
410
+ };
411
+ const onKeypress = (_text, key) => {
412
+ if (key?.ctrl && key?.name === 'c') {
413
+ cancel();
414
+ return;
415
+ }
416
+ if (key?.name === 'escape' || key?.name === 'q') {
417
+ cancel();
418
+ return;
419
+ }
420
+ if (key?.name === 'up' || key?.name === 'k') {
421
+ cursorIndex = (cursorIndex - 1 + options.length) % options.length;
422
+ }
423
+ else if (key?.name === 'down' || key?.name === 'j') {
424
+ cursorIndex = (cursorIndex + 1) % options.length;
425
+ }
426
+ else if (key?.name === 'return' || key?.name === 'enter') {
427
+ finish();
428
+ return;
429
+ }
430
+ else if (/^[1-9]$/.test(String(_text || ''))) {
431
+ const selectedIndex = Number(_text) - 1;
432
+ if (options[selectedIndex]) {
433
+ cursorIndex = selectedIndex;
434
+ finish();
435
+ return;
436
+ }
437
+ }
438
+ render();
439
+ };
440
+ process.stdin.on('keypress', onKeypress);
441
+ process.stdout.write(ANSI.hideCursor);
442
+ render();
443
+ });
444
+ }
345
445
  function normalizeConnectorProgressKey(key) {
346
446
  const normalized = String(key || '').trim().toLowerCase();
347
447
  if (normalized === 'analytics' || normalized === 'analyticscli')
@@ -439,9 +539,6 @@ function connectorStatusLabel(key, healthByConnector = {}) {
439
539
  return 'not configured';
440
540
  return `configured, ${connectorHealthLabel(health.status)}`;
441
541
  }
442
- function formatConnectorHealthLine(key, healthByConnector = {}) {
443
- return `${ANSI.dim}${formatConnectorHealthText(key, healthByConnector)}${ANSI.reset}`;
444
- }
445
542
  function formatConnectorHealthText(key, healthByConnector = {}) {
446
543
  const health = getConnectorHealth(key, healthByConnector);
447
544
  const label = connectorStatusLabel(key, healthByConnector);
@@ -596,7 +693,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
596
693
  const cancel = () => {
597
694
  cleanup();
598
695
  process.stdout.write('\n');
599
- reject(new Error('Connector setup cancelled.'));
696
+ reject(new WizardAbortError('Connector setup cancelled.'));
600
697
  };
601
698
  const toggleCurrent = () => {
602
699
  const connector = selectedDisplayConnector();
@@ -1048,11 +1145,6 @@ function printSetupSuccess(payload) {
1048
1145
  process.stdout.write(`${payload.message}\n`);
1049
1146
  }
1050
1147
  }
1051
- function healthCheckFailures(payload) {
1052
- return Array.isArray(payload?.checks)
1053
- ? payload.checks.filter((check) => check?.status === 'fail')
1054
- : [];
1055
- }
1056
1148
  function connectorFromCheckName(name) {
1057
1149
  const value = String(name || '');
1058
1150
  if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
@@ -1111,146 +1203,12 @@ function cleanHealthDetail(detail) {
1111
1203
  }
1112
1204
  return truncate(raw, 180);
1113
1205
  }
1114
- function actionForHealthFailure(failure, configPath) {
1115
- const name = String(failure?.name || '');
1116
- const detail = String(failure?.detail || '');
1117
- if (name === 'project:github-repo' || /project\.githubRepo/i.test(detail)) {
1118
- return `No action required for Sentry setup. Set project.githubRepo in ${configPath} only if you want GitHub issue/PR delivery now.`;
1119
- }
1120
- if (name.includes('analytics') || /ANALYTICSCLI|analytics/i.test(detail)) {
1121
- return 'Paste a fresh AnalyticsCLI readonly token, then let the wizard retest AnalyticsCLI.';
1122
- }
1123
- if (name.includes('sentry') || /Sentry|GlitchTip/i.test(detail)) {
1124
- return 'Only fix this if token, org, or base URL is missing or invalid.';
1125
- }
1126
- if (name.includes('github')) {
1127
- return 'Configure GitHub token/repo access, or leave GitHub delivery disabled.';
1128
- }
1129
- if (name.includes('revenuecat')) {
1130
- return 'Paste a RevenueCat v2 secret API key with read-only project permissions.';
1131
- }
1132
- if (name.includes('asc')) {
1133
- return 'Paste ASC API key details or rerun ASC setup when ready.';
1134
- }
1135
- return 'Use the connector setup flow below to refresh this configuration.';
1136
- }
1137
1206
  function isDeferredGitHubFailure(failure) {
1138
1207
  const name = String(failure?.name || '');
1139
1208
  const detail = String(failure?.detail || '');
1140
1209
  return (name === 'project:github-repo' ||
1141
1210
  (name === 'connection:github' && /project\.githubRepo|repo is missing|repo is not configured/i.test(detail)));
1142
1211
  }
1143
- function isDeferredSentryProjectFailure(failure) {
1144
- const name = String(failure?.name || '');
1145
- const detail = String(failure?.detail || '');
1146
- return name.includes('sentry') && /No Sentry projects configured/i.test(detail);
1147
- }
1148
- function summarizeHealthFailure(failure, configPath) {
1149
- const name = String(failure?.name || '');
1150
- const detail = String(failure?.detail || '');
1151
- const connector = connectorFromCheckName(`${name} ${detail}`) || 'setup';
1152
- if (connector === 'analytics' && /invalid token|unauthorized|token has been revoked/i.test(detail)) {
1153
- return {
1154
- connector,
1155
- status: 'token invalid or expired',
1156
- action: 'paste a fresh readonly token',
1157
- };
1158
- }
1159
- if (connector === 'sentry' && /No Sentry projects configured/i.test(detail)) {
1160
- return {
1161
- connector,
1162
- status: 'project scope deferred',
1163
- action: 'no user action; OpenClaw discovers visible projects from org + token',
1164
- };
1165
- }
1166
- if (connector === 'github' && isDeferredGitHubFailure(failure)) {
1167
- return {
1168
- connector,
1169
- status: 'repo not known yet',
1170
- action: `optional; set project.githubRepo in ${configPath} only for GitHub delivery`,
1171
- };
1172
- }
1173
- return {
1174
- connector,
1175
- status: cleanHealthDetail(detail),
1176
- action: actionForHealthFailure(failure, configPath),
1177
- };
1178
- }
1179
- function printHealthFailures(failures, configPath) {
1180
- const summarized = [];
1181
- const seen = new Set();
1182
- for (const failure of failures) {
1183
- if (isDeferredGitHubFailure(failure))
1184
- continue;
1185
- if (isDeferredSentryProjectFailure(failure))
1186
- continue;
1187
- const summary = summarizeHealthFailure(failure, configPath);
1188
- const key = `${summary.connector}:${summary.status}:${summary.action}`;
1189
- if (seen.has(key))
1190
- continue;
1191
- seen.add(key);
1192
- summarized.push(summary);
1193
- }
1194
- if (summarized.length === 0) {
1195
- process.stdout.write('\nOnly deferred optional checks remain.\n\n');
1196
- return;
1197
- }
1198
- process.stdout.write('\nNeeds attention\n');
1199
- process.stdout.write('---------------\n');
1200
- for (const summary of summarized) {
1201
- process.stdout.write(`- ${connectorTitle(summary.connector)}: ${summary.status}\n`);
1202
- process.stdout.write(` Next: ${summary.action}\n`);
1203
- }
1204
- process.stdout.write('\n');
1205
- }
1206
- function inferConnectorsFromHealthFailures(failures) {
1207
- const inferred = new Set();
1208
- for (const failure of failures) {
1209
- if (isDeferredGitHubFailure(failure))
1210
- continue;
1211
- if (isDeferredSentryProjectFailure(failure))
1212
- continue;
1213
- const connector = connectorFromCheckName(`${failure?.name || ''} ${failure?.detail || ''}`);
1214
- if (connector)
1215
- inferred.add(connector);
1216
- }
1217
- return orderConnectors([...inferred]);
1218
- }
1219
- async function getHealthCheckPlan(configPath, selected) {
1220
- const config = await readJsonIfPresent(configPath).catch(() => null);
1221
- const items = [
1222
- {
1223
- key: 'preflight',
1224
- label: 'Local preflight',
1225
- detail: 'config, dependencies, source wiring',
1226
- status: 'pending',
1227
- },
1228
- ];
1229
- const selectedSet = new Set(selected);
1230
- const hasAnalytics = selectedSet.has('analytics') ||
1231
- Boolean(process.env.ANALYTICSCLI_ACCESS_TOKEN?.trim() || process.env.ANALYTICSCLI_READONLY_TOKEN?.trim()) ||
1232
- (config?.sources?.analytics && config.sources.analytics.enabled !== false);
1233
- const sentryAccounts = Array.isArray(config?.sources?.sentry?.accounts) ? config.sources.sentry.accounts : [];
1234
- const hasSentry = selectedSet.has('sentry') ||
1235
- sentryAccounts.length > 0 ||
1236
- Boolean(process.env.SENTRY_AUTH_TOKEN?.trim() || process.env.GLITCHTIP_AUTH_TOKEN?.trim());
1237
- const hasRevenueCat = selectedSet.has('revenuecat') ||
1238
- Boolean(process.env.REVENUECAT_API_KEY?.trim()) ||
1239
- (config?.sources?.revenuecat && config.sources.revenuecat.enabled !== false);
1240
- const githubRepo = String(config?.project?.githubRepo || '').trim();
1241
- const hasGitHub = selectedSet.has('github') || Boolean(process.env.GITHUB_TOKEN?.trim()) || Boolean(githubRepo);
1242
- if (hasAnalytics)
1243
- items.push({ key: 'analytics', label: 'AnalyticsCLI', detail: 'token auth + readonly query', status: 'pending' });
1244
- if (hasSentry)
1245
- items.push({ key: 'sentry', label: 'Sentry / GlitchTip', detail: 'token/org API + project discovery', status: 'pending' });
1246
- if (hasRevenueCat)
1247
- items.push({ key: 'revenuecat', label: 'RevenueCat', detail: 'API key auth + project read', status: 'pending' });
1248
- if (hasGitHub && githubRepo)
1249
- items.push({ key: 'github', label: 'GitHub', detail: `repo access (${githubRepo})`, status: 'pending' });
1250
- if (hasGitHub && !githubRepo)
1251
- items.push({ key: 'github', label: 'GitHub', detail: 'skipped until repo is known', status: 'pending' });
1252
- return items;
1253
- }
1254
1212
  function healthStatusLabel(status) {
1255
1213
  if (status === 'running')
1256
1214
  return 'running';
@@ -1299,9 +1257,6 @@ function updateHealthProgress(items, event) {
1299
1257
  }
1300
1258
  return false;
1301
1259
  }
1302
- function allProgressItemsFinished(items) {
1303
- return items.length > 0 && items.every((item) => !['pending', 'running'].includes(String(item.status || '')));
1304
- }
1305
1260
  function buildSetupTestProgressPlan(selected) {
1306
1261
  const selectedSet = new Set(selected);
1307
1262
  const items = [
@@ -1430,39 +1385,6 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
1430
1385
  process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
1431
1386
  return { ok: true, retry: false, result, payload };
1432
1387
  }
1433
- async function offerConfiguredConnectionFixes(rl, configPath, selected) {
1434
- if (!(await fileExists(configPath)))
1435
- return selected;
1436
- clearTerminal();
1437
- const plan = await getHealthCheckPlan(configPath, selected);
1438
- renderHealthProgress(plan, 'Starting live checks...');
1439
- const command = `node scripts/openclaw-growth-preflight.mjs --config ${quote(configPath)} --test-connections --progress-json`;
1440
- const result = await runCommandCaptureWithProgress(command, (event) => {
1441
- if (updateHealthProgress(plan, event)) {
1442
- renderHealthProgress(plan);
1443
- }
1444
- });
1445
- renderHealthProgress(plan, 'Checks finished.');
1446
- const payload = parseJsonFromStdout(result.stdout);
1447
- const failures = healthCheckFailures(payload).filter((failure) => !isDeferredGitHubFailure(failure) && !isDeferredSentryProjectFailure(failure));
1448
- if (payload?.ok === true || failures.length === 0) {
1449
- process.stdout.write('Configured connectors look healthy.\n\n');
1450
- return selected;
1451
- }
1452
- printHealthFailures(failures, configPath);
1453
- const inferred = inferConnectorsFromHealthFailures(failures);
1454
- if (inferred.length === 0) {
1455
- process.stdout.write('Continuing with the connector(s) you selected.\n\n');
1456
- return selected;
1457
- }
1458
- const fixNow = await askYesNo(rl, `Fix now (${inferred.join(', ')})?`, true);
1459
- clearTerminal();
1460
- if (!fixNow) {
1461
- process.stdout.write('Continuing with selected connector(s).\n\n');
1462
- return selected;
1463
- }
1464
- return orderConnectors([...new Set([...selected, ...inferred])]);
1465
- }
1466
1388
  function getUserLocalBinDir() {
1467
1389
  return process.env.HOME ? path.join(process.env.HOME, '.local', 'bin') : null;
1468
1390
  }
@@ -1904,7 +1826,7 @@ function apiListItems(payload) {
1904
1826
  return payload.teams;
1905
1827
  return [];
1906
1828
  }
1907
- async function fetchSentryJsonPage({ baseUrl, token, url }) {
1829
+ async function fetchSentryJsonPage({ token, url }) {
1908
1830
  const normalizedToken = String(token || '').trim();
1909
1831
  const response = await fetch(url, {
1910
1832
  method: 'GET',
@@ -1938,7 +1860,7 @@ async function fetchSentryJsonList({ baseUrl, token, url }) {
1938
1860
  const pages = [];
1939
1861
  let nextUrl = url;
1940
1862
  for (let page = 0; nextUrl && page < 10; page += 1) {
1941
- const result = await fetchSentryJsonPage({ baseUrl, token, url: nextUrl });
1863
+ const result = await fetchSentryJsonPage({ token, url: nextUrl });
1942
1864
  pages.push(result.detail);
1943
1865
  if (!result.ok)
1944
1866
  return { ...result, payload: items, detail: pages.join('; ') };
@@ -2693,7 +2615,7 @@ async function runConnectorSetupWizard(args) {
2693
2615
  const chosenConnectors = requestedConnectors.length > 0
2694
2616
  ? orderConnectors([...new Set([...requestedConnectors, ...existingFixes])])
2695
2617
  : await askConnectorSelectionWithHealth(rl, healthByConnector, existingFixes);
2696
- let selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
2618
+ const selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
2697
2619
  if (selected.length === 0) {
2698
2620
  throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
2699
2621
  }
@@ -2964,19 +2886,38 @@ async function askCadencePlan(rl) {
2964
2886
  return cadences;
2965
2887
  }
2966
2888
  async function askWizardGoal(rl) {
2967
- process.stdout.write('\nWhat do you want to configure?\n');
2968
- process.stdout.write(' 1) Full setup: project, schedule, outputs, and sources\n');
2969
- process.stdout.write(' 2) Connectors: credentials and provider health checks\n');
2970
- process.stdout.write(' 3) Intervals: growth cadence and connector health check interval\n');
2971
- process.stdout.write(' 4) Output: summary, GitHub issues, draft PRs, and notifications\n');
2972
- const answer = await ask(rl, 'Setup area (1/2/3/4)', '1');
2973
- if (answer.trim() === '2')
2974
- return 'connectors';
2975
- if (answer.trim() === '3')
2976
- return 'intervals';
2977
- if (answer.trim() === '4')
2978
- return 'output';
2979
- return 'full';
2889
+ return await askMenuChoice(rl, {
2890
+ title: 'What do you want to configure?',
2891
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-4.',
2892
+ defaultValue: 'full',
2893
+ renderHeader: printWizardHeader,
2894
+ options: [
2895
+ {
2896
+ value: 'connectors',
2897
+ label: 'Connectors',
2898
+ detail: 'Credentials, provider setup, and health checks.',
2899
+ },
2900
+ {
2901
+ value: 'outputs_intervals',
2902
+ label: 'Outputs and intervals',
2903
+ detail: 'Daily/weekly/monthly jobs, GitHub issue/PR delivery, and OpenClaw chat notifications.',
2904
+ },
2905
+ {
2906
+ value: 'full',
2907
+ label: 'Full setup',
2908
+ detail: 'Project, connectors, outputs, intervals, and sources.',
2909
+ },
2910
+ {
2911
+ value: 'intervals',
2912
+ label: 'Advanced intervals only',
2913
+ detail: 'Runner wake-up interval and connector health check cadence.',
2914
+ },
2915
+ ],
2916
+ });
2917
+ }
2918
+ function printWizardHeader() {
2919
+ process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
2920
+ process.stdout.write('This wizard writes non-secret config only. Connector secrets stay in the local secrets file.\n\n');
2980
2921
  }
2981
2922
  async function buildDefaultWizardConfig() {
2982
2923
  const detectedRepo = await detectGitHubRepo();
@@ -3003,7 +2944,7 @@ async function buildDefaultWizardConfig() {
3003
2944
  command: getDefaultSourceCommand('revenuecat'),
3004
2945
  },
3005
2946
  sentry: {
3006
- enabled: false,
2947
+ enabled: true,
3007
2948
  mode: 'command',
3008
2949
  command: getDefaultSourceCommand('sentry'),
3009
2950
  },
@@ -3019,8 +2960,8 @@ async function buildDefaultWizardConfig() {
3019
2960
  ],
3020
2961
  },
3021
2962
  schedule: {
3022
- intervalMinutes: 1440,
3023
- connectorHealthCheckIntervalMinutes: 720,
2963
+ intervalMinutes: DEFAULT_GROWTH_INTERVAL_MINUTES,
2964
+ connectorHealthCheckIntervalMinutes: DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES,
3024
2965
  skipIfNoDataChange: true,
3025
2966
  skipIfIssueSetUnchanged: true,
3026
2967
  cadences: DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence })),
@@ -3028,6 +2969,8 @@ async function buildDefaultWizardConfig() {
3028
2969
  actions: {
3029
2970
  autoCreateIssues: false,
3030
2971
  autoCreatePullRequests: false,
2972
+ autoCreateWhenGitHubWriteAccess: true,
2973
+ disableAutoCreateGitHubArtifacts: false,
3031
2974
  mode: 'issue',
3032
2975
  usageMode: 'production_autopilot',
3033
2976
  draftPullRequests: true,
@@ -3149,9 +3092,9 @@ async function askNotificationChannels(rl, config) {
3149
3092
  }
3150
3093
  async function askOutputConfig(rl, config) {
3151
3094
  process.stdout.write('\nOutput type\n');
3152
- process.stdout.write(' 1) Summary only: OpenClaw chat handoff and notifications\n');
3153
- process.stdout.write(' 2) GitHub issue drafts: generate issue-ready handoffs, no auto-create by default\n');
3154
- process.stdout.write(' 3) GitHub pull request drafts: generate PR-oriented proposal branches when enabled\n');
3095
+ process.stdout.write(' 1) OpenClaw chat plus automatic GitHub issue fallback when repo + token allow it\n');
3096
+ process.stdout.write(' 2) GitHub issues: create issues automatically when new findings are found\n');
3097
+ process.stdout.write(' 3) GitHub pull requests: create draft PR-oriented proposal branches when enabled\n');
3155
3098
  const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3156
3099
  const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
3157
3100
  const defaultChoice = currentAutoCreate ? (currentMode === 'pull_request' ? '3' : '2') : '1';
@@ -3186,6 +3129,8 @@ async function askOutputConfig(rl, config) {
3186
3129
  mode,
3187
3130
  autoCreateIssues: mode === 'issue' && autoCreate,
3188
3131
  autoCreatePullRequests: mode === 'pull_request' && autoCreate,
3132
+ autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
3133
+ disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
3189
3134
  draftPullRequests: true,
3190
3135
  proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
3191
3136
  };
@@ -3240,8 +3185,8 @@ async function askOutputConfig(rl, config) {
3240
3185
  }
3241
3186
  async function askIntervalConfig(rl, config) {
3242
3187
  const currentSchedule = config?.schedule || {};
3243
- const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || 1440)), 10) || 1440;
3244
- const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || 720)), 10) || 720;
3188
+ const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES)), 10) || DEFAULT_GROWTH_INTERVAL_MINUTES;
3189
+ const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES)), 10) || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES;
3245
3190
  const usageMode = await askToolUsage(rl);
3246
3191
  const cadences = await askCadencePlan(rl);
3247
3192
  config.schedule = {
@@ -3258,11 +3203,15 @@ async function askIntervalConfig(rl, config) {
3258
3203
  };
3259
3204
  return config;
3260
3205
  }
3206
+ async function askOutputsAndIntervalsConfig(rl, config) {
3207
+ const withIntervals = await askIntervalConfig(rl, config);
3208
+ return await askOutputConfig(rl, withIntervals);
3209
+ }
3261
3210
  async function writeOpenClawJobManifest(configPath, config) {
3262
3211
  const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
3263
3212
  const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
3264
- const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || 1440));
3265
- const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || 720));
3213
+ const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES));
3214
+ const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES));
3266
3215
  const actionMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3267
3216
  const growthRunCommand = getGrowthRunCommand(config, displayConfigPath);
3268
3217
  const connectorHealthCommand = getConnectorHealthCommand(config, displayConfigPath);
@@ -3317,8 +3266,7 @@ async function main() {
3317
3266
  }
3318
3267
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3319
3268
  try {
3320
- process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
3321
- process.stdout.write('This wizard writes non-secret config only. Connector secrets stay in the local secrets file.\n\n');
3269
+ printWizardHeader();
3322
3270
  const goal = await askWizardGoal(rl);
3323
3271
  if (goal === 'connectors') {
3324
3272
  rl.close();
@@ -3336,15 +3284,15 @@ async function main() {
3336
3284
  process.stdout.write('OpenClaw can run and update growth jobs plus non-secret connector config from the manifest; connector API keys stay behind the connector wizard.\n');
3337
3285
  return;
3338
3286
  }
3339
- if (goal === 'output') {
3340
- const config = await askOutputConfig(rl, await loadEditableConfig(configPath));
3287
+ if (goal === 'outputs_intervals') {
3288
+ const config = await askOutputsAndIntervalsConfig(rl, await loadEditableConfig(configPath));
3341
3289
  const secretAccess = await askSecretAccessModel(rl, configPath, config);
3342
3290
  await writeJsonFile(configPath, config);
3343
3291
  const manifestPath = await writeOpenClawJobManifest(configPath, config);
3344
- process.stdout.write(`\nSaved output config: ${configPath}\n`);
3292
+ process.stdout.write(`\nSaved output and interval config: ${configPath}\n`);
3345
3293
  process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
3346
3294
  printSecretRunnerKitInstructions(secretAccess.kit);
3347
- process.stdout.write('Connector-health alerts are deduped per unhealthy incident and sent through configured channels.\n');
3295
+ process.stdout.write('Daily checks prioritize Sentry and production anomalies; larger cadences analyze all configured projects and connectors.\n');
3348
3296
  return;
3349
3297
  }
3350
3298
  const detectedRepo = await detectGitHubRepo();
@@ -3355,7 +3303,9 @@ async function main() {
3355
3303
  .map((value) => value.trim())
3356
3304
  .filter(Boolean);
3357
3305
  const maxIssues = Number.parseInt(await ask(rl, 'Max issues per run', '4'), 10) || 4;
3358
- const intervalMinutes = Number.parseInt(await ask(rl, 'Check interval in minutes', '1440'), 10) || 1440;
3306
+ const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(DEFAULT_GROWTH_INTERVAL_MINUTES)), 10) ||
3307
+ DEFAULT_GROWTH_INTERVAL_MINUTES;
3308
+ const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES)), 10) || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES;
3359
3309
  const usageMode = await askToolUsage(rl);
3360
3310
  const cadences = await askCadencePlan(rl);
3361
3311
  const actionMode = await askChoice(rl, 'Preferred GitHub artifact mode', ['issue', 'pull_request'], 'issue');
@@ -3364,7 +3314,10 @@ async function main() {
3364
3314
  defaultCommand: getDefaultSourceCommand('analytics'),
3365
3315
  });
3366
3316
  const revenuecat = await askSourceConfig(rl, 'revenuecat', 'data/openclaw-growth-engineer/revenuecat_summary.example.json', getDefaultSourceHint('revenuecat'));
3367
- const sentry = await askSourceConfig(rl, 'sentry', 'data/openclaw-growth-engineer/sentry_summary.example.json', getDefaultSourceHint('sentry'));
3317
+ const sentry = await askSourceConfig(rl, 'sentry', 'data/openclaw-growth-engineer/sentry_summary.example.json', getDefaultSourceHint('sentry'), {
3318
+ defaultEnabled: true,
3319
+ defaultCommand: getDefaultSourceCommand('sentry'),
3320
+ });
3368
3321
  const feedback = await askSourceConfig(rl, 'feedback', 'data/openclaw-growth-engineer/feedback_summary.example.json', getDefaultSourceHint('feedback'), {
3369
3322
  defaultEnabled: true,
3370
3323
  defaultCommand: getDefaultSourceCommand('feedback'),
@@ -3410,7 +3363,7 @@ async function main() {
3410
3363
  },
3411
3364
  schedule: {
3412
3365
  intervalMinutes,
3413
- connectorHealthCheckIntervalMinutes: 720,
3366
+ connectorHealthCheckIntervalMinutes,
3414
3367
  skipIfNoDataChange: true,
3415
3368
  skipIfIssueSetUnchanged: true,
3416
3369
  cadences,
@@ -3418,6 +3371,8 @@ async function main() {
3418
3371
  actions: {
3419
3372
  autoCreateIssues,
3420
3373
  autoCreatePullRequests,
3374
+ autoCreateWhenGitHubWriteAccess: true,
3375
+ disableAutoCreateGitHubArtifacts: false,
3421
3376
  mode: actionMode,
3422
3377
  usageMode,
3423
3378
  draftPullRequests: true,
@@ -3514,6 +3469,6 @@ async function main() {
3514
3469
  }
3515
3470
  main().catch((error) => {
3516
3471
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
3517
- process.exitCode = 1;
3472
+ process.exitCode = error instanceof WizardAbortError ? error.exitCode : 1;
3518
3473
  });
3519
3474
  //# sourceMappingURL=openclaw-growth-wizard.mjs.map