@analyticscli/growth-engineer 0.1.1-preview.0 → 0.1.1-preview.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +51 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +1 -0
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +460 -24
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.mjs +1 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +115 -92
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +56 -36
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +130 -36
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -11,7 +11,11 @@ const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
|
11
11
|
const DEFAULT_STATE_PATH = 'data/openclaw-growth-engineer/state.json';
|
|
12
12
|
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
13
13
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
14
|
+
const DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MULTIPLIER = 2;
|
|
15
|
+
const DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MIN_DELTA = 10;
|
|
16
|
+
const DEFAULT_DAILY_RUNNER_FAILURE_RETENTION_DAYS = 14;
|
|
14
17
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const SELF_UPDATE_SKILL_SLUG_CANDIDATES = ['growth-engineer', 'openclaw-growth-engineer'];
|
|
15
19
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
20
|
let schedulerProofPath = path.resolve(DEFAULT_SCHEDULER_PROOF_PATH);
|
|
17
21
|
const DEFAULT_CADENCES = [
|
|
@@ -144,6 +148,7 @@ function resolveRuntimeScriptPath(scriptName) {
|
|
|
144
148
|
const candidates = [
|
|
145
149
|
path.join(RUNTIME_DIR, scriptName),
|
|
146
150
|
path.resolve('scripts', scriptName),
|
|
151
|
+
path.resolve('skills/growth-engineer/scripts', scriptName),
|
|
147
152
|
path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
|
|
148
153
|
];
|
|
149
154
|
return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
|
|
@@ -247,9 +252,13 @@ function isSentryCompatibleSource(sourceConfig, sourceName) {
|
|
|
247
252
|
function shouldDegradeTransientSourceFailure(sourceConfig, sourceName, retried) {
|
|
248
253
|
if (!retried)
|
|
249
254
|
return false;
|
|
255
|
+
if (sourceConfig?.degradeTransientFailures === false)
|
|
256
|
+
return false;
|
|
250
257
|
if (!isRequiredSource(sourceConfig, sourceName))
|
|
251
258
|
return true;
|
|
252
|
-
|
|
259
|
+
if (isSentryCompatibleSource(sourceConfig, sourceName))
|
|
260
|
+
return true;
|
|
261
|
+
return sourceConfig?.degradeRequiredTransientFailures !== false;
|
|
253
262
|
}
|
|
254
263
|
function isTruthyEnv(value) {
|
|
255
264
|
return ['1', 'true', 'yes', 'y', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
@@ -327,6 +336,23 @@ async function rerunCurrentProcessWithoutSelfUpdate() {
|
|
|
327
336
|
child.on('close', (code) => resolve(code));
|
|
328
337
|
});
|
|
329
338
|
}
|
|
339
|
+
function getSelfUpdateSkillCandidates(workspaceRoot) {
|
|
340
|
+
const explicit = String(process.env.OPENCLAW_GROWTH_SKILL_SLUG || '').trim();
|
|
341
|
+
const uniqueSlugs = [...new Set([explicit, ...SELF_UPDATE_SKILL_SLUG_CANDIDATES].filter(Boolean))];
|
|
342
|
+
return uniqueSlugs.map((slug) => {
|
|
343
|
+
const skillRoot = path.join(workspaceRoot, 'skills', slug);
|
|
344
|
+
return {
|
|
345
|
+
slug,
|
|
346
|
+
skillRoot,
|
|
347
|
+
originPath: path.join(skillRoot, '.clawhub/origin.json'),
|
|
348
|
+
runnerPath: path.join(skillRoot, 'scripts/openclaw-growth-runner.mjs'),
|
|
349
|
+
bootstrapPath: path.join(skillRoot, 'scripts/bootstrap-openclaw-workspace.sh'),
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function resolveInstalledSelfUpdateSkill(workspaceRoot) {
|
|
354
|
+
return getSelfUpdateSkillCandidates(workspaceRoot).find((candidate) => existsSync(candidate.originPath)) || null;
|
|
355
|
+
}
|
|
330
356
|
async function maybeSelfUpdateFromClawHub(args) {
|
|
331
357
|
if (args.noSelfUpdate)
|
|
332
358
|
return false;
|
|
@@ -337,26 +363,27 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
337
363
|
if (isFalseyEnv(process.env.OPENCLAW_GROWTH_SELF_UPDATE))
|
|
338
364
|
return false;
|
|
339
365
|
const workspaceRoot = process.cwd();
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
366
|
+
const installedSkill = resolveInstalledSelfUpdateSkill(workspaceRoot);
|
|
367
|
+
if (!installedSkill)
|
|
342
368
|
return false;
|
|
343
369
|
if (!(await commandExists('npx')))
|
|
344
370
|
return false;
|
|
345
371
|
const force = String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
|
|
346
372
|
if (!(await shouldRunSelfUpdate(workspaceRoot, force)))
|
|
347
373
|
return false;
|
|
348
|
-
const beforeOrigin = await readJsonOptional(
|
|
374
|
+
const beforeOrigin = await readJsonOptional(installedSkill.originPath, null);
|
|
349
375
|
const beforeVersion = String(beforeOrigin?.installedVersion || '');
|
|
350
|
-
process.stdout.write(
|
|
351
|
-
const updateResult = await runShellCommand(
|
|
352
|
-
const afterOrigin = await readJsonOptional(
|
|
376
|
+
process.stdout.write(`Checking for Growth Engineer skill updates (${installedSkill.slug})...\n`);
|
|
377
|
+
const updateResult = await runShellCommand(`npx -y clawhub --no-input --dir skills update ${quote(installedSkill.slug)} --force`, 120_000);
|
|
378
|
+
const afterOrigin = await readJsonOptional(installedSkill.originPath, null);
|
|
353
379
|
const afterVersion = String(afterOrigin?.installedVersion || beforeVersion || '');
|
|
354
380
|
const workspaceRunnerPath = path.resolve(process.argv[1] || 'scripts/openclaw-growth-runner.mjs');
|
|
355
|
-
const
|
|
356
|
-
const runtimeOutdated = !(await filesHaveSameContent(workspaceRunnerPath, skillRunnerPath));
|
|
381
|
+
const runtimeOutdated = !(await filesHaveSameContent(workspaceRunnerPath, installedSkill.runnerPath));
|
|
357
382
|
await writeSelfUpdateState(workspaceRoot, {
|
|
358
383
|
lastCheckedAt: new Date().toISOString(),
|
|
359
384
|
ok: updateResult.ok,
|
|
385
|
+
skillSlug: installedSkill.slug,
|
|
386
|
+
skillRoot: installedSkill.skillRoot,
|
|
360
387
|
previousVersion: beforeVersion || null,
|
|
361
388
|
installedVersion: afterVersion || null,
|
|
362
389
|
}).catch(() => { });
|
|
@@ -370,7 +397,7 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
370
397
|
process.stdout.write(afterVersion && afterVersion !== beforeVersion
|
|
371
398
|
? `Updated OpenClaw Growth Engineer skill ${beforeVersion || 'unknown'} -> ${afterVersion}. Refreshing workspace runtime...\n`
|
|
372
399
|
: 'Refreshing workspace runtime from the installed OpenClaw Growth Engineer skill...\n');
|
|
373
|
-
const bootstrapResult = await runShellCommand(
|
|
400
|
+
const bootstrapResult = await runShellCommand(`bash ${quote(installedSkill.bootstrapPath)}`, 60_000);
|
|
374
401
|
if (!bootstrapResult.ok) {
|
|
375
402
|
process.stdout.write('Workspace runtime refresh failed; continuing with current process.\n');
|
|
376
403
|
return false;
|
|
@@ -399,6 +426,27 @@ function resolveShellCommand() {
|
|
|
399
426
|
function hardenUnattendedShellCommand(command) {
|
|
400
427
|
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
401
428
|
}
|
|
429
|
+
function redactCommandForDiagnostics(command) {
|
|
430
|
+
const raw = String(command || '').trim();
|
|
431
|
+
if (!raw)
|
|
432
|
+
return '';
|
|
433
|
+
return raw
|
|
434
|
+
.replace(/((?:^|\s)(?:[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL)[A-Z0-9_]*)=)(?:"[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]')
|
|
435
|
+
.replace(/((?:^|\s)--(?:token|access-token|api-key|key|secret|password|pass|authorization|auth|bearer)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]');
|
|
436
|
+
}
|
|
437
|
+
function truncateDiagnosticText(value, maxLength = 2000) {
|
|
438
|
+
const text = String(value || '').trim();
|
|
439
|
+
if (text.length <= maxLength)
|
|
440
|
+
return text;
|
|
441
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
442
|
+
}
|
|
443
|
+
function buildSourceCommandFailureMessage(sourceName, resolvedCommand, detail) {
|
|
444
|
+
const safeCommand = redactCommandForDiagnostics(resolvedCommand);
|
|
445
|
+
const safeDetail = truncateDiagnosticText(detail);
|
|
446
|
+
return safeCommand
|
|
447
|
+
? `Source "${sourceName}" command failed: command \`${safeCommand}\`; ${safeDetail}`
|
|
448
|
+
: `Source "${sourceName}" command failed: ${safeDetail}`;
|
|
449
|
+
}
|
|
402
450
|
function isSudoPasswordPrompt(stderr) {
|
|
403
451
|
return /sudo: (?:a password is required|a terminal is required to read the password|no tty present)/i.test(String(stderr || ''));
|
|
404
452
|
}
|
|
@@ -644,7 +692,8 @@ function buildConnectorWizardCommand(configPath, entry) {
|
|
|
644
692
|
const connector = connectorWizardKey(entry.key);
|
|
645
693
|
if (!connector)
|
|
646
694
|
return null;
|
|
647
|
-
|
|
695
|
+
const configArg = configPath ? ` --config ${quote(configPath)}` : '';
|
|
696
|
+
return `npx -y @analyticscli/growth-engineer@preview wizard${configArg} --connectors ${quote(connector)}`;
|
|
648
697
|
}
|
|
649
698
|
function conciseConnectorDetail(entry) {
|
|
650
699
|
const detail = String(entry?.detail || '').replace(/\s+/g, ' ').trim();
|
|
@@ -1042,6 +1091,323 @@ function groupIssuesByProject(issues, maxIssues = 4) {
|
|
|
1042
1091
|
}
|
|
1043
1092
|
return [...grouped.entries()];
|
|
1044
1093
|
}
|
|
1094
|
+
function getDailyIssueDedupeConfig(config) {
|
|
1095
|
+
const raw = config?.schedule?.dailyIssueDedupe || config?.notifications?.growthRun?.dailyIssueDedupe || {};
|
|
1096
|
+
const multiplier = Number(raw.eventGrowthMultiplier ?? raw.multiplier);
|
|
1097
|
+
const minDelta = Number(raw.eventGrowthMinDelta ?? raw.minDelta);
|
|
1098
|
+
return {
|
|
1099
|
+
enabled: raw.enabled !== false,
|
|
1100
|
+
eventGrowthMultiplier: Number.isFinite(multiplier) && multiplier > 1
|
|
1101
|
+
? multiplier
|
|
1102
|
+
: DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MULTIPLIER,
|
|
1103
|
+
eventGrowthMinDelta: Number.isFinite(minDelta) && minDelta > 0
|
|
1104
|
+
? minDelta
|
|
1105
|
+
: DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MIN_DELTA,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function getDailyRunnerFailureDedupeConfig(config) {
|
|
1109
|
+
const raw = config?.schedule?.dailyRunnerFailureDedupe || config?.notifications?.growthRun?.dailyRunnerFailureDedupe || {};
|
|
1110
|
+
const retentionDays = Number(raw.retentionDays);
|
|
1111
|
+
return {
|
|
1112
|
+
enabled: raw.enabled !== false,
|
|
1113
|
+
retentionDays: Number.isFinite(retentionDays) && retentionDays > 0
|
|
1114
|
+
? retentionDays
|
|
1115
|
+
: DEFAULT_DAILY_RUNNER_FAILURE_RETENTION_DAYS,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function resolveDailyIssueDedupeTimeZone(config) {
|
|
1119
|
+
return String(config?.schedule?.timezone ||
|
|
1120
|
+
config?.automation?.openclawCron?.timezone ||
|
|
1121
|
+
process.env.TZ ||
|
|
1122
|
+
'UTC').trim() || 'UTC';
|
|
1123
|
+
}
|
|
1124
|
+
function normalizeRunnerFailureForFingerprint(errorMessage) {
|
|
1125
|
+
return redactCommandForDiagnostics(errorMessage)
|
|
1126
|
+
.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g, '[timestamp]')
|
|
1127
|
+
.replace(/--since(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g, '--since [timestamp]')
|
|
1128
|
+
.replace(/--until(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g, '--until [timestamp]')
|
|
1129
|
+
.replace(/\bpid\s+\d+\b/gi, 'pid [pid]')
|
|
1130
|
+
.replace(/\s+/g, ' ')
|
|
1131
|
+
.trim();
|
|
1132
|
+
}
|
|
1133
|
+
function buildRunnerFailureFingerprint(errorMessage) {
|
|
1134
|
+
return sha256(normalizeRunnerFailureForFingerprint(errorMessage));
|
|
1135
|
+
}
|
|
1136
|
+
function pruneDailyRunnerFailures(failures, now, retentionDays) {
|
|
1137
|
+
const cutoffMs = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;
|
|
1138
|
+
return Object.fromEntries(Object.entries(failures || {}).filter(([, entry]) => {
|
|
1139
|
+
const lastSeenMs = Date.parse(String(entry?.lastSeenAt || entry?.firstSeenAt || ''));
|
|
1140
|
+
return !Number.isFinite(lastSeenMs) || lastSeenMs >= cutoffMs;
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
function parseFailureArgs(argv) {
|
|
1144
|
+
const args = {
|
|
1145
|
+
config: DEFAULT_CONFIG_PATH,
|
|
1146
|
+
state: DEFAULT_STATE_PATH,
|
|
1147
|
+
};
|
|
1148
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1149
|
+
const token = argv[i];
|
|
1150
|
+
const next = argv[i + 1];
|
|
1151
|
+
if (token === '--config' && next) {
|
|
1152
|
+
args.config = next;
|
|
1153
|
+
i += 1;
|
|
1154
|
+
}
|
|
1155
|
+
else if (token === '--state' && next) {
|
|
1156
|
+
args.state = next;
|
|
1157
|
+
i += 1;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return args;
|
|
1161
|
+
}
|
|
1162
|
+
async function recordRunnerFailure({ configPath, statePath, error, argv = [], now = new Date() }) {
|
|
1163
|
+
const errorMessage = truncateDiagnosticText(error instanceof Error ? error.message : String(error));
|
|
1164
|
+
const config = await readJsonOptional(configPath, {});
|
|
1165
|
+
const state = await readJsonOptional(statePath, {
|
|
1166
|
+
sourceHashes: {},
|
|
1167
|
+
lastIssueFingerprint: null,
|
|
1168
|
+
lastRunAt: null,
|
|
1169
|
+
sourceCursors: {},
|
|
1170
|
+
});
|
|
1171
|
+
const dedupeConfig = getDailyRunnerFailureDedupeConfig(config);
|
|
1172
|
+
const timeZone = resolveDailyIssueDedupeTimeZone(config);
|
|
1173
|
+
const date = formatDateInTimeZone(now, timeZone);
|
|
1174
|
+
const fingerprint = buildRunnerFailureFingerprint(errorMessage);
|
|
1175
|
+
const nowIso = now.toISOString();
|
|
1176
|
+
const previousDailyFailures = state?.dailyRunnerFailures?.date === date
|
|
1177
|
+
? state.dailyRunnerFailures
|
|
1178
|
+
: null;
|
|
1179
|
+
const previousFailures = previousDailyFailures?.failures && typeof previousDailyFailures.failures === 'object'
|
|
1180
|
+
? previousDailyFailures.failures
|
|
1181
|
+
: {};
|
|
1182
|
+
const failures = pruneDailyRunnerFailures(previousFailures, now, dedupeConfig.retentionDays);
|
|
1183
|
+
const previousEntry = failures[fingerprint] || null;
|
|
1184
|
+
const suppressed = dedupeConfig.enabled && Boolean(previousEntry);
|
|
1185
|
+
const nextEntry = {
|
|
1186
|
+
...(previousEntry || {}),
|
|
1187
|
+
fingerprint,
|
|
1188
|
+
error: errorMessage,
|
|
1189
|
+
normalizedError: normalizeRunnerFailureForFingerprint(errorMessage),
|
|
1190
|
+
firstSeenAt: previousEntry?.firstSeenAt || nowIso,
|
|
1191
|
+
lastSeenAt: nowIso,
|
|
1192
|
+
};
|
|
1193
|
+
if (suppressed) {
|
|
1194
|
+
nextEntry.suppressedCount = Number(previousEntry?.suppressedCount || 0) + 1;
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
nextEntry.lastReportedAt = nowIso;
|
|
1198
|
+
nextEntry.reportCount = Number(previousEntry?.reportCount || 0) + 1;
|
|
1199
|
+
}
|
|
1200
|
+
failures[fingerprint] = nextEntry;
|
|
1201
|
+
const nextState = {
|
|
1202
|
+
...state,
|
|
1203
|
+
dailyRunnerFailures: {
|
|
1204
|
+
date,
|
|
1205
|
+
timeZone,
|
|
1206
|
+
failures,
|
|
1207
|
+
updatedAt: nowIso,
|
|
1208
|
+
},
|
|
1209
|
+
lastRunnerFailure: {
|
|
1210
|
+
fingerprint,
|
|
1211
|
+
error: errorMessage,
|
|
1212
|
+
failedAt: nowIso,
|
|
1213
|
+
suppressed,
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1217
|
+
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1218
|
+
await appendSchedulerProof(suppressed ? 'runner_failed_suppressed' : 'runner_failed', {
|
|
1219
|
+
configPath,
|
|
1220
|
+
statePath,
|
|
1221
|
+
error: errorMessage,
|
|
1222
|
+
errorFingerprint: fingerprint,
|
|
1223
|
+
date,
|
|
1224
|
+
timeZone,
|
|
1225
|
+
argv,
|
|
1226
|
+
suppressed,
|
|
1227
|
+
reportCount: nextEntry.reportCount || 0,
|
|
1228
|
+
suppressedCount: nextEntry.suppressedCount || 0,
|
|
1229
|
+
socialOutput: suppressed ? 'HEARTBEAT_OK' : 'RUNNER_FAILED',
|
|
1230
|
+
socialReason: suppressed
|
|
1231
|
+
? 'runner failure unchanged and already reported today'
|
|
1232
|
+
: 'new runner failure for current day',
|
|
1233
|
+
});
|
|
1234
|
+
return {
|
|
1235
|
+
suppressed,
|
|
1236
|
+
exitCode: suppressed ? 0 : 1,
|
|
1237
|
+
fingerprint,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function formatDateInTimeZone(date, timeZone) {
|
|
1241
|
+
try {
|
|
1242
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
1243
|
+
timeZone,
|
|
1244
|
+
year: 'numeric',
|
|
1245
|
+
month: '2-digit',
|
|
1246
|
+
day: '2-digit',
|
|
1247
|
+
}).formatToParts(date);
|
|
1248
|
+
const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
1249
|
+
if (byType.year && byType.month && byType.day) {
|
|
1250
|
+
return `${byType.year}-${byType.month}-${byType.day}`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
// Fall back to UTC below for invalid host timezone settings.
|
|
1255
|
+
}
|
|
1256
|
+
return date.toISOString().slice(0, 10);
|
|
1257
|
+
}
|
|
1258
|
+
function normalizeIssueIdentityPart(value) {
|
|
1259
|
+
return String(value || '')
|
|
1260
|
+
.trim()
|
|
1261
|
+
.toLowerCase()
|
|
1262
|
+
.replace(/\s+/g, ' ');
|
|
1263
|
+
}
|
|
1264
|
+
function buildDailyIssueKey(issue) {
|
|
1265
|
+
const stableIdentity = [
|
|
1266
|
+
issueSourceUrl(issue),
|
|
1267
|
+
issue?.source_url,
|
|
1268
|
+
issue?.sourceUrl,
|
|
1269
|
+
issue?.issue_url,
|
|
1270
|
+
issue?.issueUrl,
|
|
1271
|
+
issue?.signal_id,
|
|
1272
|
+
issue?.signalId,
|
|
1273
|
+
issue?.id,
|
|
1274
|
+
]
|
|
1275
|
+
.map((value) => String(value || '').trim())
|
|
1276
|
+
.find(Boolean);
|
|
1277
|
+
const fallbackIdentity = [
|
|
1278
|
+
issueProjectLabel(issue),
|
|
1279
|
+
issue?.source,
|
|
1280
|
+
issue?.area,
|
|
1281
|
+
issue?.title,
|
|
1282
|
+
]
|
|
1283
|
+
.map(normalizeIssueIdentityPart)
|
|
1284
|
+
.filter(Boolean)
|
|
1285
|
+
.join('|');
|
|
1286
|
+
return sha256(stableIdentity || fallbackIdentity || stableStringify(issue));
|
|
1287
|
+
}
|
|
1288
|
+
function coerceIssueNumber(value) {
|
|
1289
|
+
if (value === null || value === undefined || value === '')
|
|
1290
|
+
return null;
|
|
1291
|
+
if (typeof value === 'number')
|
|
1292
|
+
return Number.isFinite(value) ? value : null;
|
|
1293
|
+
const normalized = String(value).replace(/,/g, '').trim();
|
|
1294
|
+
if (!normalized)
|
|
1295
|
+
return null;
|
|
1296
|
+
const number = Number(normalized);
|
|
1297
|
+
return Number.isFinite(number) ? number : null;
|
|
1298
|
+
}
|
|
1299
|
+
function issueEventCount(issue) {
|
|
1300
|
+
const direct = [
|
|
1301
|
+
issue?.events,
|
|
1302
|
+
issue?.eventCount,
|
|
1303
|
+
issue?.event_count,
|
|
1304
|
+
issue?.current_value,
|
|
1305
|
+
issue?.currentValue,
|
|
1306
|
+
issue?.count,
|
|
1307
|
+
];
|
|
1308
|
+
for (const value of direct) {
|
|
1309
|
+
const number = coerceIssueNumber(value);
|
|
1310
|
+
if (number !== null)
|
|
1311
|
+
return number;
|
|
1312
|
+
}
|
|
1313
|
+
const text = [
|
|
1314
|
+
issue?.impact,
|
|
1315
|
+
issue?.summary,
|
|
1316
|
+
...(Array.isArray(issue?.evidence) ? issue.evidence : []),
|
|
1317
|
+
issue?.body,
|
|
1318
|
+
]
|
|
1319
|
+
.filter(Boolean)
|
|
1320
|
+
.join('\n');
|
|
1321
|
+
const match = text.match(/\bEvents?:\s*([0-9][0-9,]*(?:\.[0-9]+)?)/i);
|
|
1322
|
+
return match ? coerceIssueNumber(match[1]) : null;
|
|
1323
|
+
}
|
|
1324
|
+
function isDrasticDailyIssueEventGrowth(currentEvents, previousEntry, dedupeConfig) {
|
|
1325
|
+
if (currentEvents === null || currentEvents === undefined)
|
|
1326
|
+
return false;
|
|
1327
|
+
const previousEvents = coerceIssueNumber(previousEntry?.lastReportedEvents ?? previousEntry?.lastSeenEvents);
|
|
1328
|
+
if (previousEvents === null || previousEvents < 0)
|
|
1329
|
+
return false;
|
|
1330
|
+
const requiredEvents = Math.max(previousEvents * dedupeConfig.eventGrowthMultiplier, previousEvents + dedupeConfig.eventGrowthMinDelta);
|
|
1331
|
+
return currentEvents >= requiredEvents;
|
|
1332
|
+
}
|
|
1333
|
+
function applyDailyIssueDedupe(issuesPayload, state, config, activeCadences, now = new Date()) {
|
|
1334
|
+
const issues = Array.isArray(issuesPayload?.issues) ? issuesPayload.issues : [];
|
|
1335
|
+
const dedupeConfig = getDailyIssueDedupeConfig(config);
|
|
1336
|
+
if (!dedupeConfig.enabled || !isShortOperationalCadence(activeCadences) || issues.length === 0) {
|
|
1337
|
+
return {
|
|
1338
|
+
issuesPayload,
|
|
1339
|
+
dailyIssueReports: state?.dailyIssueReports || null,
|
|
1340
|
+
suppressedCount: 0,
|
|
1341
|
+
reportedCount: issues.length,
|
|
1342
|
+
hasDrasticEventGrowth: false,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const timeZone = resolveDailyIssueDedupeTimeZone(config);
|
|
1346
|
+
const date = formatDateInTimeZone(now, timeZone);
|
|
1347
|
+
const nowIso = now.toISOString();
|
|
1348
|
+
const previousState = state?.dailyIssueReports?.date === date ? state.dailyIssueReports : null;
|
|
1349
|
+
const previousIssues = previousState?.issues && typeof previousState.issues === 'object'
|
|
1350
|
+
? previousState.issues
|
|
1351
|
+
: {};
|
|
1352
|
+
const nextIssues = { ...previousIssues };
|
|
1353
|
+
const reportableIssues = [];
|
|
1354
|
+
let suppressedCount = 0;
|
|
1355
|
+
let hasDrasticEventGrowth = false;
|
|
1356
|
+
for (const issue of issues) {
|
|
1357
|
+
const key = buildDailyIssueKey(issue);
|
|
1358
|
+
const previousEntry = previousIssues[key] || null;
|
|
1359
|
+
const events = issueEventCount(issue);
|
|
1360
|
+
const shouldReport = !previousEntry || isDrasticDailyIssueEventGrowth(events, previousEntry, dedupeConfig);
|
|
1361
|
+
const reportReason = !previousEntry ? 'new_daily_issue' : 'event_growth';
|
|
1362
|
+
const nextEntry = {
|
|
1363
|
+
...(previousEntry || {}),
|
|
1364
|
+
title: String(issue?.title || previousEntry?.title || '').slice(0, 240),
|
|
1365
|
+
app: issueProjectLabel(issue),
|
|
1366
|
+
sourceUrl: issueSourceUrl(issue) || previousEntry?.sourceUrl || null,
|
|
1367
|
+
lastSeenAt: nowIso,
|
|
1368
|
+
lastSeenEvents: events ?? previousEntry?.lastSeenEvents ?? null,
|
|
1369
|
+
};
|
|
1370
|
+
if (shouldReport) {
|
|
1371
|
+
reportableIssues.push(issue);
|
|
1372
|
+
if (previousEntry)
|
|
1373
|
+
hasDrasticEventGrowth = true;
|
|
1374
|
+
nextEntry.lastReportedAt = nowIso;
|
|
1375
|
+
nextEntry.lastReportedEvents = events ?? previousEntry?.lastReportedEvents ?? null;
|
|
1376
|
+
nextEntry.lastReportReason = reportReason;
|
|
1377
|
+
nextEntry.reportCount = Number(previousEntry?.reportCount || 0) + 1;
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
suppressedCount += 1;
|
|
1381
|
+
nextEntry.suppressedCount = Number(previousEntry?.suppressedCount || 0) + 1;
|
|
1382
|
+
}
|
|
1383
|
+
nextIssues[key] = nextEntry;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
issuesPayload: {
|
|
1387
|
+
...issuesPayload,
|
|
1388
|
+
issue_count: reportableIssues.length,
|
|
1389
|
+
issues: reportableIssues,
|
|
1390
|
+
suppressed_issue_count: suppressedCount,
|
|
1391
|
+
daily_issue_dedupe: {
|
|
1392
|
+
date,
|
|
1393
|
+
timeZone,
|
|
1394
|
+
suppressedCount,
|
|
1395
|
+
reportedCount: reportableIssues.length,
|
|
1396
|
+
eventGrowthMultiplier: dedupeConfig.eventGrowthMultiplier,
|
|
1397
|
+
eventGrowthMinDelta: dedupeConfig.eventGrowthMinDelta,
|
|
1398
|
+
},
|
|
1399
|
+
},
|
|
1400
|
+
dailyIssueReports: {
|
|
1401
|
+
date,
|
|
1402
|
+
timeZone,
|
|
1403
|
+
issues: nextIssues,
|
|
1404
|
+
updatedAt: nowIso,
|
|
1405
|
+
},
|
|
1406
|
+
suppressedCount,
|
|
1407
|
+
reportedCount: reportableIssues.length,
|
|
1408
|
+
hasDrasticEventGrowth,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1045
1411
|
async function deliverConnectorHealthAlert({ config, configPath, message, statusPayload, unhealthyConnectors, fingerprint }) {
|
|
1046
1412
|
const channels = getConnectorHealthChannels(config);
|
|
1047
1413
|
if (config?.notifications?.connectorHealth?.enabled === false) {
|
|
@@ -1142,6 +1508,10 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
|
|
|
1142
1508
|
? 'Action: GitHub artifact attempted.'
|
|
1143
1509
|
: 'Action: external alert only.');
|
|
1144
1510
|
}
|
|
1511
|
+
const suppressedIssueCount = Number(issuesPayload?.suppressed_issue_count || 0);
|
|
1512
|
+
if (suppressedIssueCount > 0) {
|
|
1513
|
+
lines.push(`Suppressed today: ${suppressedIssueCount} previously reported finding(s).`);
|
|
1514
|
+
}
|
|
1145
1515
|
if (charts.length > 0) {
|
|
1146
1516
|
lines.push(`Charts: ${charts.length}`);
|
|
1147
1517
|
}
|
|
@@ -1686,7 +2056,7 @@ async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorSt
|
|
|
1686
2056
|
},
|
|
1687
2057
|
};
|
|
1688
2058
|
}
|
|
1689
|
-
throw new Error(
|
|
2059
|
+
throw new Error(buildSourceCommandFailureMessage(sourceName, resolvedCommand, detail));
|
|
1690
2060
|
}
|
|
1691
2061
|
const fetchedAt = new Date().toISOString();
|
|
1692
2062
|
try {
|
|
@@ -1893,8 +2263,52 @@ async function runOnce(configPath, statePath) {
|
|
|
1893
2263
|
chartManifestPath,
|
|
1894
2264
|
cadencePlanPath,
|
|
1895
2265
|
});
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
2266
|
+
const dailyIssueDedupe = applyDailyIssueDedupe(dryRun.issuesPayload, stateAfterSourceCollection, config, activeCadences);
|
|
2267
|
+
const deliverableIssuesPayload = dailyIssueDedupe.issuesPayload;
|
|
2268
|
+
const issueFingerprint = buildIssueFingerprint(deliverableIssuesPayload);
|
|
2269
|
+
const unchangedIssueSet = issueFingerprint === stateAfterSourceCollection.lastIssueFingerprint &&
|
|
2270
|
+
!dailyIssueDedupe.hasDrasticEventGrowth;
|
|
2271
|
+
if (Number(dryRun.issuesPayload?.issue_count || 0) > 0 &&
|
|
2272
|
+
Number(deliverableIssuesPayload?.issue_count || 0) === 0 &&
|
|
2273
|
+
dailyIssueDedupe.suppressedCount > 0) {
|
|
2274
|
+
process.stdout.write(`[${new Date().toISOString()}] All findings were already reported today. Skip GitHub creation and external growth notification.\n`);
|
|
2275
|
+
const completedAt = new Date().toISOString();
|
|
2276
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
2277
|
+
await fs.writeFile(statePath, JSON.stringify({
|
|
2278
|
+
...stateAfterHealthCheck,
|
|
2279
|
+
...stateAfterSourceCollection,
|
|
2280
|
+
sourceHashes: currentHashes,
|
|
2281
|
+
sourceCursors,
|
|
2282
|
+
lastSourceFailures: sourceFailures,
|
|
2283
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
2284
|
+
lastIssueFingerprint: issueFingerprint,
|
|
2285
|
+
lastRunAt: completedAt,
|
|
2286
|
+
lastOutFile: dryRun.outFile,
|
|
2287
|
+
cadences: markCadencesRan(stateAfterSourceCollection, activeCadences, completedAt),
|
|
2288
|
+
lastGrowthRunNotifications: [
|
|
2289
|
+
{
|
|
2290
|
+
sent: false,
|
|
2291
|
+
target: 'growth_run',
|
|
2292
|
+
detail: `all ${dailyIssueDedupe.suppressedCount} finding(s) already reported today; external growth notification suppressed`,
|
|
2293
|
+
},
|
|
2294
|
+
],
|
|
2295
|
+
skippedReason: 'daily_issue_dedupe',
|
|
2296
|
+
}, null, 2), 'utf8');
|
|
2297
|
+
await appendSchedulerProof('runner_completed', {
|
|
2298
|
+
configPath,
|
|
2299
|
+
statePath,
|
|
2300
|
+
completedAt,
|
|
2301
|
+
skippedReason: 'daily_issue_dedupe',
|
|
2302
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
2303
|
+
outFile: dryRun.outFile,
|
|
2304
|
+
issueCount: Number(dryRun.issuesPayload?.issue_count || 0),
|
|
2305
|
+
suppressedIssueCount: dailyIssueDedupe.suppressedCount,
|
|
2306
|
+
sourceFailures,
|
|
2307
|
+
externalGrowthNotification: 'suppressed_daily_issue_dedupe',
|
|
2308
|
+
socialOutput: 'HEARTBEAT_OK',
|
|
2309
|
+
});
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
1898
2312
|
if (unchangedIssueSet &&
|
|
1899
2313
|
config.schedule?.skipIfIssueSetUnchanged !== false) {
|
|
1900
2314
|
process.stdout.write(`[${new Date().toISOString()}] Issue set unchanged. Skip GitHub creation and external growth notification.\n`);
|
|
@@ -1906,6 +2320,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1906
2320
|
sourceHashes: currentHashes,
|
|
1907
2321
|
sourceCursors,
|
|
1908
2322
|
lastSourceFailures: sourceFailures,
|
|
2323
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
1909
2324
|
lastIssueFingerprint: issueFingerprint,
|
|
1910
2325
|
lastRunAt: completedAt,
|
|
1911
2326
|
lastOutFile: dryRun.outFile,
|
|
@@ -1935,7 +2350,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1935
2350
|
}
|
|
1936
2351
|
const issueSetChangedOrExplicitlyAllowed = !unchangedIssueSet || config.schedule?.skipIfIssueSetUnchanged === false;
|
|
1937
2352
|
const shouldCreateGitHubArtifact = createGitHubArtifact &&
|
|
1938
|
-
Number(
|
|
2353
|
+
Number(deliverableIssuesPayload?.issue_count || 0) > 0 &&
|
|
1939
2354
|
issueSetChangedOrExplicitlyAllowed;
|
|
1940
2355
|
if (shouldCreateGitHubArtifact) {
|
|
1941
2356
|
for (const githubArtifactMode of githubArtifactModes) {
|
|
@@ -1962,6 +2377,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1962
2377
|
sourceHashes: currentHashes,
|
|
1963
2378
|
sourceCursors,
|
|
1964
2379
|
lastSourceFailures: sourceFailures,
|
|
2380
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
1965
2381
|
lastIssueFingerprint: issueFingerprint,
|
|
1966
2382
|
lastRunAt: completedAt,
|
|
1967
2383
|
lastOutFile: dryRun.outFile,
|
|
@@ -1969,7 +2385,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1969
2385
|
lastGrowthRunNotifications: await deliverGrowthRunSummary({
|
|
1970
2386
|
config,
|
|
1971
2387
|
configPath,
|
|
1972
|
-
issuesPayload:
|
|
2388
|
+
issuesPayload: deliverableIssuesPayload,
|
|
1973
2389
|
activeCadences,
|
|
1974
2390
|
sourceFiles,
|
|
1975
2391
|
fingerprint: issueFingerprint,
|
|
@@ -2010,22 +2426,42 @@ async function main() {
|
|
|
2010
2426
|
await runOnce(configPath, statePath);
|
|
2011
2427
|
}
|
|
2012
2428
|
catch (error) {
|
|
2013
|
-
await
|
|
2429
|
+
const failureDecision = await recordRunnerFailure({
|
|
2014
2430
|
configPath,
|
|
2015
2431
|
statePath,
|
|
2016
|
-
error
|
|
2017
|
-
|
|
2018
|
-
|
|
2432
|
+
error,
|
|
2433
|
+
argv: process.argv.slice(2),
|
|
2434
|
+
}).catch(async () => {
|
|
2435
|
+
await appendSchedulerProof('runner_failed', {
|
|
2436
|
+
configPath,
|
|
2437
|
+
statePath,
|
|
2438
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2439
|
+
}).catch(() => { });
|
|
2440
|
+
return null;
|
|
2441
|
+
});
|
|
2442
|
+
process.stderr.write(`[${new Date().toISOString()}] Run failed${failureDecision?.suppressed ? ' (already reported today)' : ''}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2019
2443
|
}
|
|
2020
2444
|
await sleep(intervalMinutes * 60_000);
|
|
2021
2445
|
}
|
|
2022
2446
|
}
|
|
2023
2447
|
main().catch(async (error) => {
|
|
2024
|
-
|
|
2025
|
-
|
|
2448
|
+
const fallbackArgs = parseFailureArgs(process.argv.slice(2));
|
|
2449
|
+
const configPath = path.resolve(fallbackArgs.config);
|
|
2450
|
+
const statePath = path.resolve(fallbackArgs.state);
|
|
2451
|
+
useSchedulerProofPathForStatePath(statePath);
|
|
2452
|
+
const failureDecision = await recordRunnerFailure({
|
|
2453
|
+
configPath,
|
|
2454
|
+
statePath,
|
|
2455
|
+
error,
|
|
2026
2456
|
argv: process.argv.slice(2),
|
|
2027
|
-
}).catch(() => {
|
|
2457
|
+
}).catch(async () => {
|
|
2458
|
+
await appendSchedulerProof('runner_failed', {
|
|
2459
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2460
|
+
argv: process.argv.slice(2),
|
|
2461
|
+
}).catch(() => { });
|
|
2462
|
+
return null;
|
|
2463
|
+
});
|
|
2028
2464
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
2029
|
-
process.exitCode = 1;
|
|
2465
|
+
process.exitCode = failureDecision?.exitCode ?? 1;
|
|
2030
2466
|
});
|
|
2031
2467
|
//# sourceMappingURL=openclaw-growth-runner.mjs.map
|