@analyticscli/growth-engineer 0.1.0-preview.0

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/config.d.ts +1663 -0
  4. package/dist/config.js +266 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1188 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/runtime/export-analytics-summary.d.mts +2 -0
  10. package/dist/runtime/export-analytics-summary.mjs +303 -0
  11. package/dist/runtime/export-analytics-summary.mjs.map +1 -0
  12. package/dist/runtime/export-asc-summary.d.mts +2 -0
  13. package/dist/runtime/export-asc-summary.mjs +376 -0
  14. package/dist/runtime/export-asc-summary.mjs.map +1 -0
  15. package/dist/runtime/export-revenuecat-summary.d.mts +2 -0
  16. package/dist/runtime/export-revenuecat-summary.mjs +176 -0
  17. package/dist/runtime/export-revenuecat-summary.mjs.map +1 -0
  18. package/dist/runtime/export-sentry-summary.d.mts +2 -0
  19. package/dist/runtime/export-sentry-summary.mjs +352 -0
  20. package/dist/runtime/export-sentry-summary.mjs.map +1 -0
  21. package/dist/runtime/openclaw-exporters-lib.d.mts +101 -0
  22. package/dist/runtime/openclaw-exporters-lib.mjs +1276 -0
  23. package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -0
  24. package/dist/runtime/openclaw-feedback-api.d.mts +2 -0
  25. package/dist/runtime/openclaw-feedback-api.mjs +255 -0
  26. package/dist/runtime/openclaw-feedback-api.mjs.map +1 -0
  27. package/dist/runtime/openclaw-growth-charts.py +154 -0
  28. package/dist/runtime/openclaw-growth-engineer.d.mts +2 -0
  29. package/dist/runtime/openclaw-growth-engineer.mjs +1258 -0
  30. package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -0
  31. package/dist/runtime/openclaw-growth-env.d.mts +9 -0
  32. package/dist/runtime/openclaw-growth-env.mjs +125 -0
  33. package/dist/runtime/openclaw-growth-env.mjs.map +1 -0
  34. package/dist/runtime/openclaw-growth-preflight.d.mts +2 -0
  35. package/dist/runtime/openclaw-growth-preflight.mjs +1111 -0
  36. package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -0
  37. package/dist/runtime/openclaw-growth-runner.d.mts +2 -0
  38. package/dist/runtime/openclaw-growth-runner.mjs +1302 -0
  39. package/dist/runtime/openclaw-growth-runner.mjs.map +1 -0
  40. package/dist/runtime/openclaw-growth-shared.d.mts +33 -0
  41. package/dist/runtime/openclaw-growth-shared.mjs +208 -0
  42. package/dist/runtime/openclaw-growth-shared.mjs.map +1 -0
  43. package/dist/runtime/openclaw-growth-start.d.mts +2 -0
  44. package/dist/runtime/openclaw-growth-start.mjs +1575 -0
  45. package/dist/runtime/openclaw-growth-start.mjs.map +1 -0
  46. package/dist/runtime/openclaw-growth-status.d.mts +2 -0
  47. package/dist/runtime/openclaw-growth-status.mjs +387 -0
  48. package/dist/runtime/openclaw-growth-status.mjs.map +1 -0
  49. package/dist/runtime/openclaw-growth-wizard.d.mts +2 -0
  50. package/dist/runtime/openclaw-growth-wizard.mjs +3519 -0
  51. package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -0
  52. package/dist/shell.d.ts +17 -0
  53. package/dist/shell.js +40 -0
  54. package/dist/shell.js.map +1 -0
  55. package/package.json +38 -0
  56. package/templates/analytics_summary.example.json +40 -0
  57. package/templates/config.example.json +197 -0
  58. package/templates/feedback_summary.example.json +37 -0
  59. package/templates/revenuecat_summary.example.json +25 -0
  60. package/templates/sentry_summary.example.json +23 -0
@@ -0,0 +1,1258 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { classifyServiceKind, normalizeServiceType } from './openclaw-growth-shared.mjs';
6
+ import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
7
+ const PRIORITY_WEIGHT = {
8
+ high: 3,
9
+ medium: 2,
10
+ low: 1,
11
+ };
12
+ const AREA_KEYWORDS = {
13
+ onboarding: ['onboarding', 'welcome', 'signup', 'register', 'tutorial', 'first_session'],
14
+ paywall: ['paywall', 'pricing', 'subscription', 'purchase', 'premium', 'trial', 'revenuecat'],
15
+ retention: ['retention', 'streak', 'come_back', 'session', 'habit', 'reminder', 'notification'],
16
+ conversion: ['checkout', 'purchase', 'billing', 'trial', 'subscribe', 'price'],
17
+ crash: ['error', 'exception', 'crash', 'stack', 'sentry', 'fatal'],
18
+ marketing: ['store', 'metadata', 'keyword', 'seo', 'landing', 'copy', 'conversion_copy'],
19
+ };
20
+ const DEFAULT_PROPOSALS = {
21
+ onboarding: [
22
+ 'Move highest-friction step later in onboarding and reduce required fields in first session.',
23
+ 'Add event-level instrumentation around each onboarding step to verify exact drop-off.',
24
+ 'Ship one A/B variant with shorter copy and a clearer progression indicator.',
25
+ ],
26
+ paywall: [
27
+ 'Reposition paywall after first user value moment instead of at first launch.',
28
+ 'Simplify price presentation and add one primary CTA with explicit trial wording.',
29
+ 'Track `paywall:shown`, `purchase:started`, and terminal outcomes for each flow.',
30
+ ],
31
+ retention: [
32
+ 'Add one reactivation trigger before the identified retention cliff.',
33
+ 'Instrument a cohort-specific funnel from first value action to repeat session.',
34
+ 'Run an experiment on feature nudges tied to the top retained cohort behavior.',
35
+ ],
36
+ conversion: [
37
+ 'Reduce purchase flow steps and remove optional inputs before checkout.',
38
+ 'Clarify plan differentiation and default to the strongest value package.',
39
+ 'Track abandonment reasons at each step to drive follow-up fixes.',
40
+ ],
41
+ crash: [
42
+ 'Reproduce the issue with a deterministic test case and lock a failing assertion.',
43
+ 'Harden null/undefined boundaries around failing callsites.',
44
+ 'Add telemetry breadcrumbs to isolate the exact pre-crash state.',
45
+ ],
46
+ marketing: [
47
+ 'Update App Store/landing copy to align value proposition with strongest usage signal.',
48
+ 'Refresh screenshot ordering so first two screenshots mirror top user jobs.',
49
+ 'Run one metadata experiment focused on high-intent keyword coverage.',
50
+ ],
51
+ general: [
52
+ 'Instrument the target flow with bounded analytics to validate causality.',
53
+ 'Ship the smallest possible fix behind a feature flag and compare cohorts.',
54
+ 'Document rollout and success metric before implementation starts.',
55
+ ],
56
+ };
57
+ const DEFAULT_IGNORE_DIRS = new Set([
58
+ '.git',
59
+ '.github',
60
+ '.docusaurus',
61
+ 'node_modules',
62
+ 'dist',
63
+ 'build',
64
+ '.next',
65
+ '.turbo',
66
+ 'coverage',
67
+ '.pnpm-store',
68
+ ]);
69
+ const TEXT_FILE_EXTENSIONS = new Set([
70
+ '.ts',
71
+ '.tsx',
72
+ '.js',
73
+ '.jsx',
74
+ '.mjs',
75
+ '.cjs',
76
+ '.py',
77
+ '.swift',
78
+ '.kt',
79
+ '.java',
80
+ '.go',
81
+ '.rb',
82
+ '.php',
83
+ '.yml',
84
+ '.yaml',
85
+ '.toml',
86
+ '.sql',
87
+ '.sh',
88
+ '.astro',
89
+ '.html',
90
+ '.css',
91
+ '.scss',
92
+ ]);
93
+ function parseArgs(argv) {
94
+ const args = {
95
+ analytics: null,
96
+ revenuecat: null,
97
+ sentry: null,
98
+ feedback: null,
99
+ sources: [],
100
+ repoRoot: process.cwd(),
101
+ out: 'data/openclaw-growth-engineer/issues.generated.json',
102
+ maxIssues: 5,
103
+ createIssues: false,
104
+ createPullRequests: false,
105
+ repo: null,
106
+ titlePrefix: '[Growth]',
107
+ labels: ['ai-growth', 'autogenerated'],
108
+ codeRoots: null,
109
+ chartManifest: null,
110
+ uploadCharts: true,
111
+ branchPrefix: 'openclaw/proposals',
112
+ draftPullRequests: true,
113
+ allowProposalPullRequests: false,
114
+ cadencePlan: null,
115
+ };
116
+ for (let i = 0; i < argv.length; i += 1) {
117
+ const token = argv[i];
118
+ const next = argv[i + 1];
119
+ if (token === '--') {
120
+ continue;
121
+ }
122
+ if (token === '--analytics') {
123
+ args.analytics = next;
124
+ i += 1;
125
+ }
126
+ else if (token === '--revenuecat') {
127
+ args.revenuecat = next;
128
+ i += 1;
129
+ }
130
+ else if (token === '--sentry') {
131
+ args.sentry = next;
132
+ i += 1;
133
+ }
134
+ else if (token === '--feedback') {
135
+ args.feedback = next;
136
+ i += 1;
137
+ }
138
+ else if (token === '--source') {
139
+ args.sources.push(next || '');
140
+ i += 1;
141
+ }
142
+ else if (token === '--repo-root') {
143
+ args.repoRoot = next;
144
+ i += 1;
145
+ }
146
+ else if (token === '--out') {
147
+ args.out = next;
148
+ i += 1;
149
+ }
150
+ else if (token === '--max-issues') {
151
+ args.maxIssues = Math.max(1, Number.parseInt(next, 10) || 5);
152
+ i += 1;
153
+ }
154
+ else if (token === '--create-issues') {
155
+ args.createIssues = true;
156
+ }
157
+ else if (token === '--create-pull-requests') {
158
+ args.createPullRequests = true;
159
+ }
160
+ else if (token === '--allow-proposal-pull-requests') {
161
+ args.allowProposalPullRequests = true;
162
+ }
163
+ else if (token === '--repo') {
164
+ args.repo = next;
165
+ i += 1;
166
+ }
167
+ else if (token === '--title-prefix') {
168
+ args.titlePrefix = next;
169
+ i += 1;
170
+ }
171
+ else if (token === '--labels') {
172
+ args.labels = (next || '')
173
+ .split(',')
174
+ .map((value) => value.trim())
175
+ .filter(Boolean);
176
+ i += 1;
177
+ }
178
+ else if (token === '--code-roots') {
179
+ args.codeRoots = (next || '')
180
+ .split(',')
181
+ .map((value) => value.trim())
182
+ .filter(Boolean);
183
+ i += 1;
184
+ }
185
+ else if (token === '--chart-manifest') {
186
+ args.chartManifest = next;
187
+ i += 1;
188
+ }
189
+ else if (token === '--no-upload-charts') {
190
+ args.uploadCharts = false;
191
+ }
192
+ else if (token === '--branch-prefix') {
193
+ args.branchPrefix = next || args.branchPrefix;
194
+ i += 1;
195
+ }
196
+ else if (token === '--no-draft-pull-requests') {
197
+ args.draftPullRequests = false;
198
+ }
199
+ else if (token === '--cadence-plan') {
200
+ args.cadencePlan = next;
201
+ i += 1;
202
+ }
203
+ else if (token === '--help' || token === '-h') {
204
+ printHelpAndExit(0);
205
+ }
206
+ else {
207
+ printHelpAndExit(1, `Unknown argument: ${token}`);
208
+ }
209
+ }
210
+ if (!args.analytics) {
211
+ printHelpAndExit(1, 'Missing required argument: --analytics <file>');
212
+ }
213
+ if (args.createIssues && args.createPullRequests) {
214
+ printHelpAndExit(1, 'Use either --create-issues or --create-pull-requests, not both.');
215
+ }
216
+ return args;
217
+ }
218
+ function printHelpAndExit(exitCode, reason = null) {
219
+ if (reason) {
220
+ process.stderr.write(`${reason}\n\n`);
221
+ }
222
+ const help = `
223
+ OpenClaw Growth Engineer MVP
224
+
225
+ Usage:
226
+ node scripts/openclaw-growth-engineer.mjs --analytics <file> [options]
227
+
228
+ Required:
229
+ --analytics <file> Analytics summary JSON
230
+
231
+ Optional:
232
+ --revenuecat <file> RevenueCat summary JSON
233
+ --sentry <file> Sentry summary JSON
234
+ --feedback <file> User feedback summary JSON (optional)
235
+ --source <k=file> Additional connector summary JSON (repeatable)
236
+ --repo-root <dir> Repository root to scan (default: current directory)
237
+ --out <file> Output JSON with generated issue drafts
238
+ --max-issues <n> Max number of issues to generate (default: 5)
239
+ --create-issues Create issues directly on GitHub
240
+ --create-pull-requests Create proposal-only draft PRs on GitHub
241
+ --allow-proposal-pull-requests
242
+ Required with --create-pull-requests to acknowledge these PRs contain only .openclaw/proposals/*.md files
243
+ --repo <owner/name> GitHub repository (required with GitHub creation mode)
244
+ --title-prefix <text> Title prefix (default: [Growth])
245
+ --labels <a,b,c> Comma-separated GitHub labels
246
+ --code-roots <a,b> Prioritized code roots (default: auto-detect apps,packages)
247
+ --chart-manifest <f> Optional chart manifest JSON (signal_id -> png path)
248
+ --no-upload-charts Skip uploading charts to GitHub repo during issue creation
249
+ --branch-prefix <p> Branch prefix for generated proposal PRs (default: openclaw/proposals)
250
+ --no-draft-pull-requests Create non-draft PRs when using --create-pull-requests
251
+ --cadence-plan <file> Optional JSON with due cadence instructions for this run
252
+ --help Show this help
253
+
254
+ Environment:
255
+ GITHUB_TOKEN GitHub token with issue or pull-request write permissions
256
+ `;
257
+ process.stdout.write(help);
258
+ process.exit(exitCode);
259
+ }
260
+ async function readJson(filePath) {
261
+ const raw = await fs.readFile(filePath, 'utf8');
262
+ return JSON.parse(raw);
263
+ }
264
+ function normalizeSignals(payload, source, service = source) {
265
+ if (!payload || typeof payload !== 'object') {
266
+ return [];
267
+ }
268
+ const result = [];
269
+ const serviceKind = classifyServiceKind(service);
270
+ if (Array.isArray(payload.signals)) {
271
+ for (const signal of payload.signals) {
272
+ result.push({
273
+ source,
274
+ id: String(signal.id || `${source}_${result.length + 1}`),
275
+ title: String(signal.title || signal.summary || 'Untitled signal'),
276
+ area: String(signal.area || inferAreaFromText(signal.title || signal.summary || '')),
277
+ priority: String(signal.priority || 'medium').toLowerCase(),
278
+ evidence: toStringArray(signal.evidence),
279
+ suggestedActions: toStringArray(signal.suggested_actions || signal.suggestedActions),
280
+ keywords: toStringArray(signal.keywords),
281
+ metric: signal.metric ? String(signal.metric) : null,
282
+ deltaPercent: coerceNumber(signal.delta_percent ?? signal.deltaPercent),
283
+ currentValue: coerceNumber(signal.current_value ?? signal.currentValue),
284
+ baselineValue: coerceNumber(signal.baseline_value ?? signal.baselineValue),
285
+ confidence: signal.confidence ? String(signal.confidence) : null,
286
+ });
287
+ }
288
+ }
289
+ if (serviceKind === 'crash' && Array.isArray(payload.issues)) {
290
+ for (const issue of payload.issues) {
291
+ result.push({
292
+ source,
293
+ id: String(issue.id || `sentry_${result.length + 1}`),
294
+ title: String(issue.title || issue.issue || 'Untitled sentry issue'),
295
+ area: String(issue.area || 'crash'),
296
+ priority: String(issue.priority || issue.level || 'high').toLowerCase(),
297
+ evidence: toStringArray([
298
+ issue.impact ? `Impact: ${issue.impact}` : null,
299
+ issue.events ? `Events: ${issue.events}` : null,
300
+ issue.users ? `Affected users: ${issue.users}` : null,
301
+ ...(Array.isArray(issue.evidence) ? issue.evidence : []),
302
+ ]),
303
+ suggestedActions: toStringArray(issue.suggested_actions || issue.suggestedActions),
304
+ keywords: toStringArray(issue.stack_keywords || issue.keywords),
305
+ metric: issue.metric ? String(issue.metric) : 'crash_rate',
306
+ deltaPercent: coerceNumber(issue.delta_percent ?? issue.deltaPercent),
307
+ currentValue: null,
308
+ baselineValue: null,
309
+ confidence: issue.confidence ? String(issue.confidence) : null,
310
+ });
311
+ }
312
+ }
313
+ if (serviceKind === 'feedback') {
314
+ const items = Array.isArray(payload.items)
315
+ ? payload.items
316
+ : Array.isArray(payload.feedback)
317
+ ? payload.feedback
318
+ : Array.isArray(payload.signals)
319
+ ? payload.signals
320
+ : [];
321
+ for (const item of items) {
322
+ result.push({
323
+ source,
324
+ id: String(item.id || `feedback_${result.length + 1}`),
325
+ title: String(item.title || item.summary || item.request || 'User feedback signal'),
326
+ area: String(item.area || inferAreaFromText(item.title || item.summary || item.request || '')),
327
+ priority: String(item.priority || inferFeedbackPriority(item)).toLowerCase(),
328
+ evidence: toStringArray([
329
+ item.comment ? `Comment: ${item.comment}` : null,
330
+ item.count ? `Mentions: ${item.count}` : null,
331
+ item.channel ? `Channel: ${item.channel}` : null,
332
+ ...(Array.isArray(item.locations)
333
+ ? item.locations.map((location) => {
334
+ if (!location || typeof location !== 'object')
335
+ return null;
336
+ const locationId = location.location_id ?? location.locationId ?? location.id ?? location.name ?? null;
337
+ const count = location.count ?? null;
338
+ if (!locationId)
339
+ return null;
340
+ return count ? `Location: ${locationId} (${count})` : `Location: ${locationId}`;
341
+ })
342
+ : []),
343
+ item.location ? `Location: ${item.location}` : null,
344
+ item.locationId ? `Location: ${item.locationId}` : null,
345
+ ...(Array.isArray(item.evidence) ? item.evidence : []),
346
+ ]),
347
+ suggestedActions: toStringArray(item.suggested_actions || item.suggestedActions),
348
+ keywords: toStringArray(item.keywords || item.tags),
349
+ metric: item.metric ? String(item.metric) : 'feedback_mentions',
350
+ deltaPercent: coerceNumber(item.delta_percent ?? item.deltaPercent),
351
+ currentValue: coerceNumber(item.count ?? item.current_value ?? item.currentValue),
352
+ baselineValue: coerceNumber(item.baseline_count ?? item.baseline_value ?? item.baselineValue),
353
+ confidence: item.confidence ? String(item.confidence) : null,
354
+ });
355
+ }
356
+ }
357
+ return result;
358
+ }
359
+ function getRecognizedSignalContainers(payload, service = 'custom') {
360
+ if (!payload || typeof payload !== 'object') {
361
+ return [];
362
+ }
363
+ const containers = [];
364
+ const serviceKind = classifyServiceKind(service);
365
+ if (Array.isArray(payload.signals)) {
366
+ containers.push(`signals[${payload.signals.length}]`);
367
+ }
368
+ if (serviceKind === 'crash' && Array.isArray(payload.issues)) {
369
+ containers.push(`issues[${payload.issues.length}]`);
370
+ }
371
+ if (serviceKind === 'feedback') {
372
+ if (Array.isArray(payload.items)) {
373
+ containers.push(`items[${payload.items.length}]`);
374
+ }
375
+ if (Array.isArray(payload.feedback)) {
376
+ containers.push(`feedback[${payload.feedback.length}]`);
377
+ }
378
+ }
379
+ return containers;
380
+ }
381
+ function getPayloadWarnings(payload) {
382
+ const warnings = payload?.meta?.queryWarnings;
383
+ return Array.isArray(warnings) ? warnings.map((warning) => String(warning)).filter(Boolean) : [];
384
+ }
385
+ function summarizeNoSignalsSource({ payload, source, service }) {
386
+ if (!payload || typeof payload !== 'object') {
387
+ return `${source}: no JSON object payload`;
388
+ }
389
+ const containers = getRecognizedSignalContainers(payload, service);
390
+ const warnings = getPayloadWarnings(payload);
391
+ const shape = containers.length > 0
392
+ ? containers.join(', ')
393
+ : `unrecognized top-level keys: ${Object.keys(payload).slice(0, 8).join(', ') || '(none)'}`;
394
+ return `${source}: ${shape}${warnings.length > 0 ? `; warnings: ${warnings.join(' | ')}` : ''}`;
395
+ }
396
+ function buildNoSignalsError(sourceEntries) {
397
+ const summaries = sourceEntries.map(summarizeNoSignalsSource);
398
+ const hasRecognizedShape = sourceEntries.some((entry) => getRecognizedSignalContainers(entry.payload, entry.service).length > 0);
399
+ const headline = hasRecognizedShape
400
+ ? 'No actionable growth signals found. Source JSON shape is recognized, but all signal containers are empty.'
401
+ : 'No signals found. Check input JSON shape (expected: signals[], crash issues[], or feedback items[]).';
402
+ return [
403
+ headline,
404
+ 'Sources:',
405
+ ...summaries.map((summary) => `- ${summary}`),
406
+ 'Next steps:',
407
+ '- Leave AnalyticsCLI project scope unpinned by default; the exporter scans all accessible projects unless a task explicitly needs `--project <id>`.',
408
+ '- Verify accessible AnalyticsCLI projects have release analytics events in the requested window, or enable additional sources such as ASC CLI, RevenueCat, Sentry, or feedback.',
409
+ ].join('\n');
410
+ }
411
+ function inferFeedbackPriority(item) {
412
+ const mentionCount = Number(item.count ?? 0);
413
+ const lower = `${item.title ?? ''} ${item.comment ?? ''}`.toLowerCase();
414
+ if (mentionCount >= 25)
415
+ return 'high';
416
+ if (lower.includes('crash') || lower.includes('cannot') || lower.includes('blocked'))
417
+ return 'high';
418
+ if (mentionCount >= 10)
419
+ return 'medium';
420
+ return 'low';
421
+ }
422
+ function toStringArray(value) {
423
+ if (!value)
424
+ return [];
425
+ if (Array.isArray(value)) {
426
+ return value
427
+ .filter((item) => item !== undefined && item !== null)
428
+ .map((item) => String(item).trim())
429
+ .filter(Boolean);
430
+ }
431
+ const normalized = String(value).trim();
432
+ return normalized ? [normalized] : [];
433
+ }
434
+ function coerceNumber(value) {
435
+ const number = Number(value);
436
+ return Number.isFinite(number) ? number : null;
437
+ }
438
+ function inferAreaFromText(text) {
439
+ const lower = text.toLowerCase();
440
+ for (const [area, keywords] of Object.entries(AREA_KEYWORDS)) {
441
+ if (keywords.some((keyword) => lower.includes(keyword))) {
442
+ return area;
443
+ }
444
+ }
445
+ return 'general';
446
+ }
447
+ async function collectRepoFiles(repoRoot) {
448
+ const files = [];
449
+ const root = path.resolve(repoRoot);
450
+ async function walk(current) {
451
+ const entries = await fs.readdir(current, { withFileTypes: true });
452
+ for (const entry of entries) {
453
+ const fullPath = path.join(current, entry.name);
454
+ const relativePath = path.relative(root, fullPath);
455
+ if (!relativePath || relativePath.startsWith('..')) {
456
+ continue;
457
+ }
458
+ if (entry.isDirectory()) {
459
+ if (DEFAULT_IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) {
460
+ continue;
461
+ }
462
+ await walk(fullPath);
463
+ }
464
+ else if (entry.isFile()) {
465
+ const ext = path.extname(entry.name).toLowerCase();
466
+ if (TEXT_FILE_EXTENSIONS.has(ext)) {
467
+ files.push(relativePath);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ await walk(root);
473
+ return files;
474
+ }
475
+ async function readFileSnippet(repoRoot, relativePath, bytes = 20000) {
476
+ const fullPath = path.join(repoRoot, relativePath);
477
+ try {
478
+ const content = await fs.readFile(fullPath, 'utf8');
479
+ return content.slice(0, bytes);
480
+ }
481
+ catch {
482
+ return '';
483
+ }
484
+ }
485
+ async function findRelevantFiles(repoRoot, allFiles, signal, maxFiles = 5) {
486
+ const baseKeywords = new Set([
487
+ ...(AREA_KEYWORDS[signal.area] || []),
488
+ ...signal.keywords,
489
+ ...tokenize(signal.title),
490
+ ...(signal.metric ? tokenize(signal.metric) : []),
491
+ ]);
492
+ const keywords = [...baseKeywords].filter((word) => word.length > 2);
493
+ const scored = [];
494
+ for (const file of allFiles) {
495
+ let score = pathScoreBias(file);
496
+ const lowerPath = file.toLowerCase();
497
+ for (const keyword of keywords) {
498
+ if (lowerPath.includes(keyword.toLowerCase())) {
499
+ score += 4;
500
+ }
501
+ }
502
+ if (score > 0) {
503
+ scored.push({ file, score });
504
+ continue;
505
+ }
506
+ const snippet = await readFileSnippet(repoRoot, file);
507
+ if (!snippet)
508
+ continue;
509
+ const lowerContent = snippet.toLowerCase();
510
+ for (const keyword of keywords) {
511
+ const re = new RegExp(`\\b${escapeRegExp(keyword.toLowerCase())}\\b`, 'g');
512
+ const matches = lowerContent.match(re);
513
+ if (matches) {
514
+ score += matches.length;
515
+ }
516
+ }
517
+ if (score > 0) {
518
+ scored.push({ file, score });
519
+ }
520
+ }
521
+ scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
522
+ return scored.slice(0, maxFiles).map((entry) => entry.file);
523
+ }
524
+ function pathScoreBias(relativePath) {
525
+ let score = 0;
526
+ if (relativePath.startsWith('apps/'))
527
+ score += 6;
528
+ if (relativePath.startsWith('packages/'))
529
+ score += 6;
530
+ if (relativePath.includes('/docs/') || relativePath.startsWith('apps/docs/'))
531
+ score -= 10;
532
+ if (relativePath.includes('/.github/') || relativePath.startsWith('.github/'))
533
+ score -= 10;
534
+ if (relativePath.startsWith('skills/') ||
535
+ relativePath.startsWith('docs/') ||
536
+ relativePath.startsWith('agent/') ||
537
+ relativePath.startsWith('data/')) {
538
+ score -= 8;
539
+ }
540
+ if (relativePath === 'scripts/openclaw-growth-engineer.mjs') {
541
+ score -= 12;
542
+ }
543
+ return score;
544
+ }
545
+ function pickCodeRoots(files, explicitRoots) {
546
+ if (explicitRoots && explicitRoots.length > 0) {
547
+ return explicitRoots;
548
+ }
549
+ const defaults = ['apps', 'packages'];
550
+ const available = defaults.filter((root) => files.some((file) => file.startsWith(`${root}/`)));
551
+ return available.length > 0 ? available : [];
552
+ }
553
+ function filterFilesByRoots(files, roots) {
554
+ if (!roots || roots.length === 0) {
555
+ return files;
556
+ }
557
+ return files.filter((file) => roots.some((root) => file.startsWith(`${root}/`)));
558
+ }
559
+ function escapeRegExp(value) {
560
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
561
+ }
562
+ function tokenize(text) {
563
+ return String(text)
564
+ .toLowerCase()
565
+ .split(/[^a-z0-9]+/)
566
+ .filter(Boolean);
567
+ }
568
+ function buildIssueDraft(signal, matchedFiles, titlePrefix, activeCadences = []) {
569
+ const title = `${titlePrefix} ${signal.title}`.trim();
570
+ const proposals = signal.suggestedActions.length > 0
571
+ ? signal.suggestedActions
572
+ : DEFAULT_PROPOSALS[signal.area] || DEFAULT_PROPOSALS.general;
573
+ const evidence = [...signal.evidence];
574
+ if (signal.metric && signal.currentValue !== null && signal.baselineValue !== null) {
575
+ evidence.push(`Metric \`${signal.metric}\`: current=${signal.currentValue}, baseline=${signal.baselineValue}`);
576
+ }
577
+ if (signal.deltaPercent !== null) {
578
+ evidence.push(`Delta: ${signal.deltaPercent}%`);
579
+ }
580
+ if (evidence.length === 0) {
581
+ evidence.push('No explicit evidence provided in source payload.');
582
+ }
583
+ const expectedImpact = inferExpectedImpact(signal);
584
+ const confidence = signal.confidence || inferConfidence(signal);
585
+ const nextStepPrompt = buildPrPrompt(signal, matchedFiles, proposals);
586
+ const body = [
587
+ '## Problem',
588
+ signal.title,
589
+ '',
590
+ ...(activeCadences.length > 0
591
+ ? [
592
+ '## Operating Cadence',
593
+ ...activeCadences.map((cadence) => {
594
+ const detail = [cadence.objective, cadence.instructions].filter(Boolean).join(' ');
595
+ return `- ${cadence.title}: ${detail || 'Focus this investigation around the active cadence.'}`;
596
+ }),
597
+ '',
598
+ ]
599
+ : []),
600
+ '## Evidence',
601
+ ...evidence.map((line) => `- ${line}`),
602
+ '',
603
+ '## Affected Files / Modules',
604
+ ...(matchedFiles.length > 0
605
+ ? matchedFiles.map((file) => `- \`${file}\``)
606
+ : ['- No high-confidence file match found. Start from flow owner modules.']),
607
+ '',
608
+ '## Proposed Implementation',
609
+ ...proposals.map((line) => `- ${line}`),
610
+ '',
611
+ '## Expected Impact',
612
+ expectedImpact,
613
+ '',
614
+ '## Confidence',
615
+ confidence,
616
+ '',
617
+ '## Optional Next-Step PR Prompt',
618
+ '```text',
619
+ nextStepPrompt,
620
+ '```',
621
+ ].join('\n');
622
+ return {
623
+ source: signal.source,
624
+ signal_id: signal.id,
625
+ title,
626
+ body,
627
+ priority: signal.priority,
628
+ area: signal.area,
629
+ files: matchedFiles,
630
+ expected_impact: expectedImpact,
631
+ confidence,
632
+ cadences: activeCadences.map((cadence) => cadence.key),
633
+ };
634
+ }
635
+ function inferExpectedImpact(signal) {
636
+ if (signal.source === 'sentry' || signal.area === 'crash') {
637
+ return 'Reduce crash-driven funnel exits and recover blocked conversions.';
638
+ }
639
+ if (signal.area === 'marketing') {
640
+ return 'Increase top-of-funnel acquisition quality and store listing conversion.';
641
+ }
642
+ if (signal.area === 'paywall' || signal.area === 'conversion') {
643
+ return 'Increase trial start and paid conversion rates in primary monetization flow.';
644
+ }
645
+ if (signal.area === 'onboarding' || signal.area === 'retention') {
646
+ return 'Lift activation and short-term retention in first user sessions.';
647
+ }
648
+ return 'Improve product performance on the tracked KPI with measurable experimentation.';
649
+ }
650
+ function inferConfidence(signal) {
651
+ if (signal.priority === 'high' && signal.evidence.length >= 2) {
652
+ return 'High';
653
+ }
654
+ if (signal.evidence.length >= 1) {
655
+ return 'Medium';
656
+ }
657
+ return 'Low';
658
+ }
659
+ function buildPrPrompt(signal, files, proposals) {
660
+ const primaryFiles = files.length > 0 ? files.slice(0, 4).join(', ') : 'flow owner files to be identified';
661
+ const actions = proposals.slice(0, 3).map((item) => `- ${item}`).join('\n');
662
+ return [
663
+ `Implement issue: ${signal.title}.`,
664
+ `Focus area: ${signal.area}.`,
665
+ `Likely files: ${primaryFiles}.`,
666
+ 'Requirements:',
667
+ actions,
668
+ '- Add or update tests for the changed behavior.',
669
+ '- Add analytics instrumentation to verify post-release impact.',
670
+ '- Keep changes scoped and production-safe.',
671
+ ].join('\n');
672
+ }
673
+ function rankSignals(signals) {
674
+ return [...signals].sort((a, b) => {
675
+ const pa = PRIORITY_WEIGHT[a.priority] || 1;
676
+ const pb = PRIORITY_WEIGHT[b.priority] || 1;
677
+ if (pa !== pb)
678
+ return pb - pa;
679
+ const da = Math.abs(a.deltaPercent ?? 0);
680
+ const db = Math.abs(b.deltaPercent ?? 0);
681
+ if (da !== db)
682
+ return db - da;
683
+ return a.title.localeCompare(b.title);
684
+ });
685
+ }
686
+ function dedupeSignals(signals) {
687
+ const seen = new Set();
688
+ const result = [];
689
+ for (const signal of signals) {
690
+ const key = `${signal.area}:${signal.title.toLowerCase()}`;
691
+ if (seen.has(key))
692
+ continue;
693
+ seen.add(key);
694
+ result.push(signal);
695
+ }
696
+ return result;
697
+ }
698
+ function normalizeCadencePlan(payload) {
699
+ const rawCadences = Array.isArray(payload?.cadences)
700
+ ? payload.cadences
701
+ : Array.isArray(payload)
702
+ ? payload
703
+ : [];
704
+ return rawCadences
705
+ .filter((cadence) => cadence && typeof cadence === 'object')
706
+ .map((cadence) => ({
707
+ key: String(cadence.key || cadence.id || 'custom'),
708
+ title: String(cadence.title || cadence.label || cadence.key || 'Custom cadence'),
709
+ objective: String(cadence.objective || ''),
710
+ instructions: String(cadence.instructions || ''),
711
+ focusAreas: toStringArray(cadence.focusAreas || cadence.focus_areas).map((value) => value.toLowerCase()),
712
+ sourcePriorities: toStringArray(cadence.sourcePriorities || cadence.source_priorities).map((value) => value.toLowerCase()),
713
+ criticalOnly: cadence.criticalOnly === true || cadence.critical_only === true,
714
+ }));
715
+ }
716
+ function signalSearchText(signal) {
717
+ return [
718
+ signal.source,
719
+ signal.area,
720
+ signal.title,
721
+ signal.metric,
722
+ ...signal.evidence,
723
+ ...signal.suggestedActions,
724
+ ...signal.keywords,
725
+ ]
726
+ .filter(Boolean)
727
+ .join(' ')
728
+ .toLowerCase();
729
+ }
730
+ function cadenceSearchText(cadence) {
731
+ return [
732
+ cadence.title,
733
+ cadence.objective,
734
+ cadence.instructions,
735
+ ...cadence.focusAreas,
736
+ ...cadence.sourcePriorities,
737
+ ]
738
+ .filter(Boolean)
739
+ .join(' ')
740
+ .toLowerCase();
741
+ }
742
+ function isCriticalSignal(signal) {
743
+ const text = signalSearchText(signal);
744
+ const metric = String(signal.metric || '').toLowerCase();
745
+ if (signal.priority === 'high')
746
+ return true;
747
+ if (signal.area === 'crash')
748
+ return true;
749
+ if (['sentry', 'glitchtip', 'crashlytics', 'bugsnag'].some((source) => String(signal.source).includes(source))) {
750
+ return true;
751
+ }
752
+ if (/(crash|fatal|exception|error|outage|blocked|regression|failed release|production)/i.test(text)) {
753
+ return true;
754
+ }
755
+ if (/(conversion|purchase|revenue|trial|checkout|paywall|user|active|activation)/i.test(metric)) {
756
+ const current = Number(signal.currentValue);
757
+ const baseline = Number(signal.baselineValue);
758
+ if (Number.isFinite(signal.deltaPercent) && signal.deltaPercent <= -20)
759
+ return true;
760
+ if (Number.isFinite(current) && Number.isFinite(baseline) && baseline > 0 && current / baseline <= 0.75) {
761
+ return true;
762
+ }
763
+ }
764
+ return /(very low|near zero|zero|drop|dropped|collapse|collapsed|anomaly|spike)/i.test(text);
765
+ }
766
+ function cadenceSignalScore(signal, cadences) {
767
+ if (!cadences.length)
768
+ return 0;
769
+ const text = signalSearchText(signal);
770
+ let score = 0;
771
+ for (const cadence of cadences) {
772
+ if (cadence.focusAreas.includes(signal.area))
773
+ score += 8;
774
+ if (cadence.sourcePriorities.includes(String(signal.source).toLowerCase()))
775
+ score += 6;
776
+ if (cadence.criticalOnly && isCriticalSignal(signal))
777
+ score += 12;
778
+ for (const keyword of tokenize(cadenceSearchText(cadence))) {
779
+ if (keyword.length > 3 && text.includes(keyword))
780
+ score += 1;
781
+ }
782
+ }
783
+ return score;
784
+ }
785
+ function applyCadencePlan(signals, cadences) {
786
+ if (!cadences.length)
787
+ return signals;
788
+ const allCriticalOnly = cadences.every((cadence) => cadence.criticalOnly);
789
+ const filtered = allCriticalOnly ? signals.filter(isCriticalSignal) : signals;
790
+ return [...filtered].sort((a, b) => cadenceSignalScore(b, cadences) - cadenceSignalScore(a, cadences));
791
+ }
792
+ function indexChartsBySignal(manifest, manifestFilePath) {
793
+ const bySignal = new Map();
794
+ if (!manifest || typeof manifest !== 'object') {
795
+ return bySignal;
796
+ }
797
+ const charts = Array.isArray(manifest.charts) ? manifest.charts : [];
798
+ for (const chart of charts) {
799
+ const signalId = String(chart.signal_id || chart.signalId || '').trim();
800
+ if (!signalId)
801
+ continue;
802
+ const filePathRaw = String(chart.file_path || chart.filePath || '').trim();
803
+ if (!filePathRaw)
804
+ continue;
805
+ const resolvedPath = path.isAbsolute(filePathRaw)
806
+ ? filePathRaw
807
+ : manifestFilePath
808
+ ? path.resolve(path.dirname(manifestFilePath), filePathRaw)
809
+ : path.resolve(filePathRaw);
810
+ const entry = {
811
+ signal_id: signalId,
812
+ file_path: resolvedPath,
813
+ caption: String(chart.caption || chart.title || 'Data chart'),
814
+ };
815
+ const existing = bySignal.get(signalId) || [];
816
+ existing.push(entry);
817
+ bySignal.set(signalId, existing);
818
+ }
819
+ return bySignal;
820
+ }
821
+ function appendChartsSection(body, charts, mode) {
822
+ if (!charts || charts.length === 0) {
823
+ return body;
824
+ }
825
+ const lines = ['', '## Data Chart'];
826
+ for (const chart of charts) {
827
+ if (mode === 'remote') {
828
+ lines.push(`![${chart.caption}](${chart.url})`);
829
+ }
830
+ else {
831
+ lines.push(`- ${chart.caption}: \`${chart.file_path}\``);
832
+ }
833
+ }
834
+ return `${body}\n${lines.join('\n')}`;
835
+ }
836
+ function slugify(value) {
837
+ return String(value || '')
838
+ .toLowerCase()
839
+ .replace(/[^a-z0-9]+/g, '-')
840
+ .replace(/^-+|-+$/g, '')
841
+ .slice(0, 64);
842
+ }
843
+ function stripDataChartSection(body) {
844
+ return body.replace(/\n## Data Chart[\s\S]*$/m, '');
845
+ }
846
+ function buildProposalMarkdown(draft, proposalPath) {
847
+ return [
848
+ `# ${draft.title}`,
849
+ '',
850
+ 'This proposal file was generated by OpenClaw growth autopilot.',
851
+ `It describes the requested change before implementation starts.`,
852
+ '',
853
+ `- Signal ID: \`${draft.signal_id}\``,
854
+ `- Source: \`${draft.source}\``,
855
+ `- Proposal path: \`${proposalPath}\``,
856
+ '',
857
+ stripDataChartSection(draft.body),
858
+ '',
859
+ ].join('\n');
860
+ }
861
+ async function githubApiRequest(url, token, options = {}) {
862
+ const response = await fetch(url, {
863
+ ...options,
864
+ headers: {
865
+ Authorization: `Bearer ${token}`,
866
+ Accept: 'application/vnd.github+json',
867
+ 'Content-Type': 'application/json',
868
+ 'User-Agent': 'openclaw-growth-engineer-mvp',
869
+ ...(options.headers || {}),
870
+ },
871
+ });
872
+ return response;
873
+ }
874
+ async function getRepoDefaultBranch(repo, token) {
875
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}`, token);
876
+ if (!response.ok) {
877
+ const text = await response.text();
878
+ throw new Error(`Failed to fetch repo metadata (${response.status}): ${text}`);
879
+ }
880
+ const payload = await response.json();
881
+ return String(payload.default_branch || 'main');
882
+ }
883
+ async function getBranchRefSha(repo, token, branch) {
884
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/git/ref/heads/${encodeURIComponent(branch)}`, token, { method: 'GET' });
885
+ if (!response.ok) {
886
+ const text = await response.text();
887
+ throw new Error(`Failed to fetch branch ref (${response.status}): ${text}`);
888
+ }
889
+ const payload = await response.json();
890
+ const sha = payload?.object?.sha;
891
+ if (!sha || typeof sha !== 'string') {
892
+ throw new Error(`Branch ref for ${branch} did not contain a commit SHA.`);
893
+ }
894
+ return sha;
895
+ }
896
+ async function createBranchRef(repo, token, branch, sha) {
897
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/git/refs`, token, {
898
+ method: 'POST',
899
+ body: JSON.stringify({
900
+ ref: `refs/heads/${branch}`,
901
+ sha,
902
+ }),
903
+ });
904
+ if (!response.ok) {
905
+ const text = await response.text();
906
+ throw new Error(`Failed to create branch ${branch} (${response.status}): ${text}`);
907
+ }
908
+ }
909
+ async function getContentSha(repo, token, branch, targetPath) {
910
+ const encoded = targetPath.split('/').map((part) => encodeURIComponent(part)).join('/');
911
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/contents/${encoded}?ref=${encodeURIComponent(branch)}`, token, { method: 'GET' });
912
+ if (response.status === 404) {
913
+ return null;
914
+ }
915
+ if (!response.ok) {
916
+ const text = await response.text();
917
+ throw new Error(`Failed to fetch content SHA (${response.status}): ${text}`);
918
+ }
919
+ const payload = await response.json();
920
+ return payload.sha ? String(payload.sha) : null;
921
+ }
922
+ async function putRepoTextFile({ repo, token, branch, targetPath, content, message }) {
923
+ const contentBase64 = Buffer.from(content, 'utf8').toString('base64');
924
+ const existingSha = await getContentSha(repo, token, branch, targetPath);
925
+ const encodedPath = targetPath.split('/').map((part) => encodeURIComponent(part)).join('/');
926
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/contents/${encodedPath}`, token, {
927
+ method: 'PUT',
928
+ body: JSON.stringify({
929
+ message,
930
+ content: contentBase64,
931
+ branch,
932
+ ...(existingSha ? { sha: existingSha } : {}),
933
+ }),
934
+ });
935
+ if (!response.ok) {
936
+ const text = await response.text();
937
+ throw new Error(`Failed to write proposal file (${response.status}): ${text}`);
938
+ }
939
+ }
940
+ async function uploadFileToRepo({ repo, token, branch, targetPath, sourcePath }) {
941
+ const fileBytes = await fs.readFile(sourcePath);
942
+ const contentBase64 = fileBytes.toString('base64');
943
+ const existingSha = await getContentSha(repo, token, branch, targetPath);
944
+ const body = {
945
+ message: `chore(openclaw): add chart asset ${path.basename(targetPath)}`,
946
+ content: contentBase64,
947
+ branch,
948
+ };
949
+ if (existingSha) {
950
+ body.sha = existingSha;
951
+ }
952
+ const encodedPath = targetPath.split('/').map((part) => encodeURIComponent(part)).join('/');
953
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/contents/${encodedPath}`, token, {
954
+ method: 'PUT',
955
+ body: JSON.stringify(body),
956
+ });
957
+ if (!response.ok) {
958
+ const text = await response.text();
959
+ throw new Error(`Failed to upload chart (${response.status}): ${text}`);
960
+ }
961
+ return `https://raw.githubusercontent.com/${repo}/${branch}/${targetPath}`;
962
+ }
963
+ async function createGithubIssue({ repo, title, body, labels, token }) {
964
+ const response = await fetch(`https://api.github.com/repos/${repo}/issues`, {
965
+ method: 'POST',
966
+ headers: {
967
+ Authorization: `Bearer ${token}`,
968
+ Accept: 'application/vnd.github+json',
969
+ 'Content-Type': 'application/json',
970
+ 'User-Agent': 'openclaw-growth-engineer-mvp',
971
+ },
972
+ body: JSON.stringify({
973
+ title,
974
+ body,
975
+ labels,
976
+ }),
977
+ });
978
+ if (!response.ok) {
979
+ const text = await response.text();
980
+ throw new Error(`GitHub issue creation failed (${response.status}): ${text}`);
981
+ }
982
+ return response.json();
983
+ }
984
+ async function createGithubPullRequest({ repo, title, body, head, base, draft, token }) {
985
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/pulls`, token, {
986
+ method: 'POST',
987
+ body: JSON.stringify({
988
+ title,
989
+ body,
990
+ head,
991
+ base,
992
+ draft,
993
+ }),
994
+ });
995
+ if (!response.ok) {
996
+ const text = await response.text();
997
+ throw new Error(`GitHub pull request creation failed (${response.status}): ${text}`);
998
+ }
999
+ return response.json();
1000
+ }
1001
+ async function addLabelsToGithubIssue({ repo, number, labels, token }) {
1002
+ if (!labels || labels.length === 0) {
1003
+ return;
1004
+ }
1005
+ const response = await githubApiRequest(`https://api.github.com/repos/${repo}/issues/${number}/labels`, token, {
1006
+ method: 'POST',
1007
+ body: JSON.stringify({
1008
+ labels,
1009
+ }),
1010
+ });
1011
+ if (!response.ok) {
1012
+ const text = await response.text();
1013
+ throw new Error(`GitHub label apply failed (${response.status}): ${text}`);
1014
+ }
1015
+ }
1016
+ async function ensureParentDir(filePath) {
1017
+ const directory = path.dirname(filePath);
1018
+ await fs.mkdir(directory, { recursive: true });
1019
+ }
1020
+ async function createProposalPullRequest({ repo, token, draft, labels, branchPrefix, draftPullRequests, }) {
1021
+ const dateSegment = new Date().toISOString().slice(0, 10);
1022
+ const slug = slugify(draft.signal_id || draft.title || `proposal-${Date.now()}`) || `proposal-${Date.now()}`;
1023
+ const baseBranch = await getRepoDefaultBranch(repo, token);
1024
+ const baseSha = await getBranchRefSha(repo, token, baseBranch);
1025
+ const branchName = `${String(branchPrefix || 'openclaw/proposals').replace(/\/+$/g, '')}/${dateSegment}/${slug}-${Date.now().toString(36)}`;
1026
+ await createBranchRef(repo, token, branchName, baseSha);
1027
+ const proposalPath = `.openclaw/proposals/${dateSegment}/${slug}.md`;
1028
+ const proposalMarkdown = buildProposalMarkdown(draft, proposalPath);
1029
+ await putRepoTextFile({
1030
+ repo,
1031
+ token,
1032
+ branch: branchName,
1033
+ targetPath: proposalPath,
1034
+ content: proposalMarkdown,
1035
+ message: `docs(openclaw): add proposal for ${draft.title}`,
1036
+ });
1037
+ const prBody = [
1038
+ '## Why this PR exists',
1039
+ 'This proposal-only draft PR was generated automatically from product signals and repo context.',
1040
+ 'It intentionally adds only a proposal file. It does not implement production app changes.',
1041
+ '',
1042
+ '## Requested change',
1043
+ `- Proposal file: \`${proposalPath}\``,
1044
+ `- Source connector: \`${draft.source}\``,
1045
+ `- Confidence: ${draft.confidence}`,
1046
+ '',
1047
+ '## Suggested implementation scope',
1048
+ ...(draft.files.length > 0
1049
+ ? draft.files.map((file) => `- \`${file}\``)
1050
+ : ['- No high-confidence file match yet. Start from the owning flow modules.']),
1051
+ '',
1052
+ '## Next step',
1053
+ 'OpenClaw or a developer must implement production code changes in a separate implementation PR, or convert this proposal PR by adding real app changes.',
1054
+ ].join('\n');
1055
+ const pr = await createGithubPullRequest({
1056
+ repo,
1057
+ token,
1058
+ title: draft.title,
1059
+ body: prBody,
1060
+ head: branchName,
1061
+ base: baseBranch,
1062
+ draft: draftPullRequests,
1063
+ });
1064
+ await addLabelsToGithubIssue({
1065
+ repo,
1066
+ number: pr.number,
1067
+ labels,
1068
+ token,
1069
+ });
1070
+ return {
1071
+ title: pr.title,
1072
+ number: pr.number,
1073
+ url: pr.html_url,
1074
+ branch: branchName,
1075
+ proposalPath,
1076
+ };
1077
+ }
1078
+ async function main() {
1079
+ await loadOpenClawGrowthSecrets();
1080
+ const args = parseArgs(process.argv.slice(2));
1081
+ const repoRoot = path.resolve(args.repoRoot);
1082
+ const outputPath = path.resolve(args.out);
1083
+ const analytics = await readJson(path.resolve(args.analytics));
1084
+ const revenuecat = args.revenuecat ? await readJson(path.resolve(args.revenuecat)) : null;
1085
+ const sentry = args.sentry ? await readJson(path.resolve(args.sentry)) : null;
1086
+ const feedback = args.feedback ? await readJson(path.resolve(args.feedback)) : null;
1087
+ const extraSources = [];
1088
+ for (const entry of args.sources) {
1089
+ const [rawKey, rawFilePath] = String(entry || '').split('=');
1090
+ const service = normalizeServiceType(rawKey || '');
1091
+ const key = service.replace(/-/g, '_');
1092
+ const filePath = String(rawFilePath || '').trim();
1093
+ if (!key || !filePath) {
1094
+ throw new Error(`Invalid --source value: ${entry}. Expected <key>=<file>.`);
1095
+ }
1096
+ extraSources.push({
1097
+ key,
1098
+ service,
1099
+ payload: await readJson(path.resolve(filePath)),
1100
+ });
1101
+ }
1102
+ const chartManifestPath = args.chartManifest ? path.resolve(args.chartManifest) : null;
1103
+ const chartManifest = chartManifestPath ? await readJson(chartManifestPath) : null;
1104
+ const chartsBySignal = indexChartsBySignal(chartManifest, chartManifestPath);
1105
+ const cadencePlanPath = args.cadencePlan ? path.resolve(args.cadencePlan) : null;
1106
+ const activeCadences = cadencePlanPath ? normalizeCadencePlan(await readJson(cadencePlanPath)) : [];
1107
+ const rankedSignals = dedupeSignals(rankSignals([
1108
+ ...normalizeSignals(analytics, 'analytics', 'analytics'),
1109
+ ...normalizeSignals(revenuecat, 'revenuecat', 'revenuecat'),
1110
+ ...normalizeSignals(sentry, 'sentry', 'sentry'),
1111
+ ...normalizeSignals(feedback, 'feedback', 'feedback'),
1112
+ ...extraSources.flatMap((source) => normalizeSignals(source.payload, source.key, source.service)),
1113
+ ]));
1114
+ const signals = applyCadencePlan(rankedSignals, activeCadences).slice(0, args.maxIssues);
1115
+ if (signals.length === 0) {
1116
+ const allCriticalOnly = activeCadences.length > 0 && activeCadences.every((cadence) => cadence.criticalOnly);
1117
+ if (allCriticalOnly && rankedSignals.length > 0) {
1118
+ const output = {
1119
+ generated_at: new Date().toISOString(),
1120
+ repo_root: repoRoot,
1121
+ run_cadences: activeCadences,
1122
+ issue_count: 0,
1123
+ issues: [],
1124
+ summary: 'No critical production or business-anomaly signals matched the active cadence.',
1125
+ };
1126
+ await ensureParentDir(outputPath);
1127
+ await fs.writeFile(outputPath, JSON.stringify(output, null, 2), 'utf8');
1128
+ process.stdout.write(`No critical signals matched active cadence. Wrote ${outputPath}\n`);
1129
+ return;
1130
+ }
1131
+ throw new Error(buildNoSignalsError([
1132
+ { payload: analytics, source: 'analytics', service: 'analytics' },
1133
+ ...(revenuecat ? [{ payload: revenuecat, source: 'revenuecat', service: 'revenuecat' }] : []),
1134
+ ...(sentry ? [{ payload: sentry, source: 'sentry', service: 'sentry' }] : []),
1135
+ ...(feedback ? [{ payload: feedback, source: 'feedback', service: 'feedback' }] : []),
1136
+ ...extraSources.map((source) => ({
1137
+ payload: source.payload,
1138
+ source: source.key,
1139
+ service: source.service,
1140
+ })),
1141
+ ]));
1142
+ }
1143
+ const files = await collectRepoFiles(repoRoot);
1144
+ const codeRoots = pickCodeRoots(files, args.codeRoots);
1145
+ const scopedFiles = filterFilesByRoots(files, codeRoots);
1146
+ const issueDrafts = [];
1147
+ for (const signal of signals) {
1148
+ const matchedFiles = await findRelevantFiles(repoRoot, scopedFiles, signal, 6);
1149
+ const draft = buildIssueDraft(signal, matchedFiles, args.titlePrefix, activeCadences);
1150
+ const localCharts = chartsBySignal.get(draft.signal_id) || [];
1151
+ if (localCharts.length > 0) {
1152
+ draft.body = appendChartsSection(draft.body, localCharts, 'local');
1153
+ draft.charts = localCharts;
1154
+ }
1155
+ issueDrafts.push(draft);
1156
+ }
1157
+ const output = {
1158
+ generated_at: new Date().toISOString(),
1159
+ repo_root: repoRoot,
1160
+ run_cadences: activeCadences,
1161
+ issue_count: issueDrafts.length,
1162
+ issues: issueDrafts,
1163
+ };
1164
+ await ensureParentDir(outputPath);
1165
+ await fs.writeFile(outputPath, JSON.stringify(output, null, 2), 'utf8');
1166
+ process.stdout.write(`Generated ${issueDrafts.length} issue draft(s): ${outputPath}\n`);
1167
+ if (!args.createIssues && !args.createPullRequests) {
1168
+ process.stdout.write('Dry run only. Re-run with --create-issues or --create-pull-requests --repo <owner/name> to create GitHub artifacts.\n');
1169
+ return;
1170
+ }
1171
+ if (args.createPullRequests && !args.allowProposalPullRequests) {
1172
+ throw new Error('--create-pull-requests only creates proposal-only markdown PRs. Use --allow-proposal-pull-requests when explicitly requested, or have OpenClaw implement code changes in the target repo instead.');
1173
+ }
1174
+ if (!args.repo) {
1175
+ throw new Error('Missing --repo <owner/name> while using GitHub creation mode.');
1176
+ }
1177
+ const token = process.env.GITHUB_TOKEN;
1178
+ if (!token) {
1179
+ throw new Error('Missing GITHUB_TOKEN environment variable.');
1180
+ }
1181
+ const defaultBranch = args.createIssues && args.uploadCharts ? await getRepoDefaultBranch(args.repo, token) : null;
1182
+ const created = [];
1183
+ if (args.createIssues) {
1184
+ for (const draft of issueDrafts) {
1185
+ let body = draft.body;
1186
+ if (args.uploadCharts && defaultBranch) {
1187
+ const localCharts = chartsBySignal.get(draft.signal_id) || [];
1188
+ if (localCharts.length > 0) {
1189
+ const remoteCharts = [];
1190
+ for (const [index, chart] of localCharts.entries()) {
1191
+ try {
1192
+ const extension = path.extname(chart.file_path) || '.png';
1193
+ const chartTargetPath = `.openclaw/charts/${new Date().toISOString().slice(0, 10)}/${draft.signal_id}_${index + 1}${extension}`;
1194
+ const url = await uploadFileToRepo({
1195
+ repo: args.repo,
1196
+ token,
1197
+ branch: defaultBranch,
1198
+ targetPath: chartTargetPath,
1199
+ sourcePath: chart.file_path,
1200
+ });
1201
+ remoteCharts.push({ caption: chart.caption, url });
1202
+ }
1203
+ catch (error) {
1204
+ process.stderr.write(`Chart upload failed for ${chart.file_path}: ${error instanceof Error ? error.message : String(error)}\n`);
1205
+ }
1206
+ }
1207
+ if (remoteCharts.length > 0) {
1208
+ body = appendChartsSection(stripDataChartSection(body), remoteCharts, 'remote');
1209
+ }
1210
+ }
1211
+ }
1212
+ const issue = await createGithubIssue({
1213
+ repo: args.repo,
1214
+ title: draft.title,
1215
+ body,
1216
+ labels: args.labels,
1217
+ token,
1218
+ });
1219
+ created.push({
1220
+ type: 'issue',
1221
+ title: issue.title,
1222
+ number: issue.number,
1223
+ url: issue.html_url,
1224
+ });
1225
+ process.stdout.write(`Created issue #${issue.number}: ${issue.html_url}\n`);
1226
+ }
1227
+ }
1228
+ else {
1229
+ for (const draft of issueDrafts) {
1230
+ const pr = await createProposalPullRequest({
1231
+ repo: args.repo,
1232
+ token,
1233
+ draft,
1234
+ labels: args.labels,
1235
+ branchPrefix: args.branchPrefix,
1236
+ draftPullRequests: args.draftPullRequests,
1237
+ });
1238
+ created.push({
1239
+ type: 'pull_request',
1240
+ ...pr,
1241
+ });
1242
+ process.stdout.write(`Created pull request #${pr.number}: ${pr.url}\n`);
1243
+ }
1244
+ }
1245
+ const createdPath = outputPath.replace(/\.json$/i, '.created.json');
1246
+ await fs.writeFile(createdPath, JSON.stringify({
1247
+ generated_at: new Date().toISOString(),
1248
+ repo: args.repo,
1249
+ mode: args.createPullRequests ? 'pull_request' : 'issue',
1250
+ created,
1251
+ }, null, 2), 'utf8');
1252
+ process.stdout.write(`Saved created issue metadata to ${createdPath}\n`);
1253
+ }
1254
+ main().catch((error) => {
1255
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1256
+ process.exitCode = 1;
1257
+ });
1258
+ //# sourceMappingURL=openclaw-growth-engineer.mjs.map