@analyticscli/growth-engineer 0.1.0-preview.12 → 0.1.0-preview.14
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/config.d.ts +161 -34
- package/dist/config.js +19 -1
- package/dist/config.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +294 -3
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/openclaw-exporters-lib.d.mts +1 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +8 -6
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +25 -6
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +214 -74
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +6 -0
- package/dist/runtime/openclaw-growth-shared.mjs +54 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +170 -5
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +3 -25
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +160 -126
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/config.example.json +15 -1
|
@@ -5,14 +5,15 @@ import process from 'node:process';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { getActionMode, getAllSourceEntries, getGitHubArtifactModes, getGitHubRequirementText, shouldAutoCreateGitHubArtifact, } from './openclaw-growth-shared.mjs';
|
|
8
|
+
import { deriveRuntimeDirFromStatePath, deriveSchedulerProofPathFromStatePath, getActionMode, getAllSourceEntries, getGitHubArtifactModes, getGitHubRequirementText, shouldAutoCreateGitHubArtifact, } from './openclaw-growth-shared.mjs';
|
|
9
9
|
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
10
10
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
11
11
|
const DEFAULT_STATE_PATH = 'data/openclaw-growth-engineer/state.json';
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
13
13
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
14
14
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15
15
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
let schedulerProofPath = path.resolve(DEFAULT_SCHEDULER_PROOF_PATH);
|
|
16
17
|
const DEFAULT_CADENCES = [
|
|
17
18
|
{
|
|
18
19
|
key: 'daily',
|
|
@@ -146,6 +147,24 @@ function replaceLegacyRuntimeScriptCommand(command) {
|
|
|
146
147
|
return trimmed;
|
|
147
148
|
return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-engineer\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
148
149
|
}
|
|
150
|
+
function commandHasConfigArg(command) {
|
|
151
|
+
return /(?:^|\s)--config(?:=|\s|$)/.test(String(command || ''));
|
|
152
|
+
}
|
|
153
|
+
function commandShouldReceiveActiveConfig(command) {
|
|
154
|
+
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-sentry-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
155
|
+
}
|
|
156
|
+
function withActiveConfigArg(command, configPath) {
|
|
157
|
+
const trimmed = String(command || '').trim();
|
|
158
|
+
if (!trimmed || !configPath || !commandShouldReceiveActiveConfig(trimmed)) {
|
|
159
|
+
return trimmed;
|
|
160
|
+
}
|
|
161
|
+
if (commandHasConfigArg(trimmed)) {
|
|
162
|
+
return trimmed
|
|
163
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`)
|
|
164
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`);
|
|
165
|
+
}
|
|
166
|
+
return `${trimmed} --config ${quote(configPath)}`;
|
|
167
|
+
}
|
|
149
168
|
async function readJson(filePath) {
|
|
150
169
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
151
170
|
return JSON.parse(raw);
|
|
@@ -161,6 +180,22 @@ async function readJsonOptional(filePath, fallback) {
|
|
|
161
180
|
async function ensureDir(dirPath) {
|
|
162
181
|
await fs.mkdir(dirPath, { recursive: true });
|
|
163
182
|
}
|
|
183
|
+
async function appendSchedulerProof(event, details = {}) {
|
|
184
|
+
const proofPath = schedulerProofPath;
|
|
185
|
+
const entry = {
|
|
186
|
+
ts: new Date().toISOString(),
|
|
187
|
+
event,
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
cwd: process.cwd(),
|
|
190
|
+
...details,
|
|
191
|
+
};
|
|
192
|
+
await fs.mkdir(path.dirname(proofPath), { recursive: true });
|
|
193
|
+
await fs.appendFile(proofPath, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
function useSchedulerProofPathForStatePath(statePath) {
|
|
196
|
+
schedulerProofPath = path.resolve(deriveSchedulerProofPathFromStatePath(statePath));
|
|
197
|
+
return schedulerProofPath;
|
|
198
|
+
}
|
|
164
199
|
function sha256(input) {
|
|
165
200
|
return createHash('sha256').update(input).digest('hex');
|
|
166
201
|
}
|
|
@@ -529,7 +564,7 @@ function buildConnectorHealthAlert(statusPayload, unhealthyConnectors) {
|
|
|
529
564
|
lines.push(` Next: ${entry.nextAction}`);
|
|
530
565
|
}
|
|
531
566
|
if (entry.key === 'appStoreConnect' && entry.status === 'partial') {
|
|
532
|
-
lines.push(' Note: ASC
|
|
567
|
+
lines.push(' Note: ASC uses API-key batch reports by default. Experimental ASC web analytics should only be requested when a needed metric is unavailable through API reports.');
|
|
533
568
|
}
|
|
534
569
|
}
|
|
535
570
|
lines.push('');
|
|
@@ -550,20 +585,43 @@ async function writeConnectorHealthAlert(runtimeDir, message, statusPayload, unh
|
|
|
550
585
|
}, null, 2), 'utf8');
|
|
551
586
|
return { markdownPath, jsonPath };
|
|
552
587
|
}
|
|
553
|
-
function
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
return
|
|
588
|
+
function notificationChannelKey(channel) {
|
|
589
|
+
const type = String(channel?.type || 'openclaw-chat');
|
|
590
|
+
if (type === 'openclaw-chat')
|
|
591
|
+
return 'openclaw-chat';
|
|
592
|
+
if (type === 'slack')
|
|
593
|
+
return `slack:${channel?.label || channel?.webhookEnv || 'slack'}`;
|
|
594
|
+
if (type === 'webhook')
|
|
595
|
+
return `webhook:${channel?.label || channel?.urlEnv || channel?.webhookEnv || 'webhook'}`;
|
|
596
|
+
if (type === 'command')
|
|
597
|
+
return `command:${channel?.label || channel?.command || 'command'}`;
|
|
598
|
+
return `${type}:${channel?.label || type}`;
|
|
599
|
+
}
|
|
600
|
+
function mergeNotificationChannelsWithDeliveries(configuredChannels, deliveryChannels) {
|
|
601
|
+
const configured = Array.isArray(configuredChannels) ? configuredChannels : [];
|
|
602
|
+
const seen = new Set(configured.map((channel) => notificationChannelKey(channel)));
|
|
603
|
+
const channels = configured.filter((channel) => channel?.enabled !== false);
|
|
604
|
+
for (const channel of deliveryChannels) {
|
|
605
|
+
if (!seen.has(notificationChannelKey(channel))) {
|
|
606
|
+
channels.push(channel);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return channels;
|
|
610
|
+
}
|
|
611
|
+
function getDeliveryNotificationChannels(config, kind) {
|
|
559
612
|
const channels = [];
|
|
560
613
|
const deliveries = config?.deliveries || {};
|
|
561
614
|
if (deliveries.openclawChat?.enabled) {
|
|
615
|
+
const isConnectorHealth = kind === 'connectorHealth';
|
|
562
616
|
channels.push({
|
|
563
617
|
type: 'openclaw-chat',
|
|
564
618
|
label: 'openclaw_chat',
|
|
565
|
-
markdownPath:
|
|
566
|
-
|
|
619
|
+
markdownPath: isConnectorHealth
|
|
620
|
+
? deliveries.openclawChat.connectorHealthMarkdownPath || deliveries.openclawChat.markdownPath
|
|
621
|
+
: deliveries.openclawChat.growthRunMarkdownPath || '.openclaw/chat/growth-summary.md',
|
|
622
|
+
jsonPath: isConnectorHealth
|
|
623
|
+
? deliveries.openclawChat.connectorHealthJsonPath || deliveries.openclawChat.jsonPath
|
|
624
|
+
: deliveries.openclawChat.growthRunJsonPath || '.openclaw/chat/growth-summary.json',
|
|
567
625
|
});
|
|
568
626
|
}
|
|
569
627
|
if (deliveries.slack?.enabled) {
|
|
@@ -582,19 +640,37 @@ function getConnectorHealthChannels(config) {
|
|
|
582
640
|
headers: deliveries.webhook.headers || {},
|
|
583
641
|
});
|
|
584
642
|
}
|
|
643
|
+
if (deliveries.command?.enabled) {
|
|
644
|
+
channels.push({
|
|
645
|
+
type: 'command',
|
|
646
|
+
label: deliveries.command.label || 'command',
|
|
647
|
+
command: deliveries.command.command || '',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
585
650
|
if (deliveries.discord?.enabled) {
|
|
586
651
|
channels.push({
|
|
587
652
|
type: 'command',
|
|
588
|
-
label: 'discord',
|
|
589
|
-
command: deliveries.discord.command || '
|
|
653
|
+
label: deliveries.discord.label || 'discord',
|
|
654
|
+
command: deliveries.discord.command || '',
|
|
590
655
|
});
|
|
591
656
|
}
|
|
592
657
|
return channels;
|
|
593
658
|
}
|
|
659
|
+
function getConnectorHealthChannels(config) {
|
|
660
|
+
const configuredChannels = Array.isArray(config?.notifications?.connectorHealth?.channels)
|
|
661
|
+
? config.notifications.connectorHealth.channels
|
|
662
|
+
: [];
|
|
663
|
+
return mergeNotificationChannelsWithDeliveries(configuredChannels, getDeliveryNotificationChannels(config, 'connectorHealth'));
|
|
664
|
+
}
|
|
665
|
+
function resolveOpenClawChatDeliveryPath(channelPath, fallbackPath) {
|
|
666
|
+
const targetPath = String(channelPath || fallbackPath || '').trim();
|
|
667
|
+
if (!targetPath)
|
|
668
|
+
return path.resolve(process.cwd(), fallbackPath);
|
|
669
|
+
return path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
|
|
670
|
+
}
|
|
594
671
|
async function writeConfiguredOpenClawChatAlert(configPath, channel, message, statusPayload, unhealthyConnectors, fingerprint) {
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
const jsonPath = path.resolve(baseDir, channel.jsonPath || '.openclaw/chat/connector-health.json');
|
|
672
|
+
const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/connector-health.md');
|
|
673
|
+
const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/connector-health.json');
|
|
598
674
|
await fs.mkdir(path.dirname(markdownPath), { recursive: true });
|
|
599
675
|
await fs.mkdir(path.dirname(jsonPath), { recursive: true });
|
|
600
676
|
await fs.writeFile(markdownPath, message, 'utf8');
|
|
@@ -607,8 +683,9 @@ async function writeConfiguredOpenClawChatAlert(configPath, channel, message, st
|
|
|
607
683
|
}, null, 2), 'utf8');
|
|
608
684
|
return {
|
|
609
685
|
sent: true,
|
|
686
|
+
external: false,
|
|
610
687
|
target: channel.label || 'openclaw_chat',
|
|
611
|
-
detail: `wrote ${markdownPath} and ${jsonPath}`,
|
|
688
|
+
detail: `wrote local OpenClaw chat outbox ${markdownPath} and ${jsonPath}`,
|
|
612
689
|
};
|
|
613
690
|
}
|
|
614
691
|
async function sendSlackConnectorHealthAlert(channel, message) {
|
|
@@ -624,6 +701,7 @@ async function sendSlackConnectorHealthAlert(channel, message) {
|
|
|
624
701
|
});
|
|
625
702
|
return {
|
|
626
703
|
sent: response.ok,
|
|
704
|
+
external: true,
|
|
627
705
|
target: channel.label || 'slack',
|
|
628
706
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
629
707
|
};
|
|
@@ -651,6 +729,7 @@ async function sendWebhookConnectorHealthAlert(channel, message, statusPayload,
|
|
|
651
729
|
});
|
|
652
730
|
return {
|
|
653
731
|
sent: response.ok,
|
|
732
|
+
external: true,
|
|
654
733
|
target: channel.label || 'webhook',
|
|
655
734
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
656
735
|
};
|
|
@@ -662,10 +741,17 @@ async function sendCommandConnectorHealthAlert(channel, message) {
|
|
|
662
741
|
const result = await runShellCommand(String(channel.command), 60_000, { input: message });
|
|
663
742
|
return {
|
|
664
743
|
sent: result.ok,
|
|
744
|
+
external: true,
|
|
665
745
|
target: channel.label || 'command',
|
|
666
746
|
detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
667
747
|
};
|
|
668
748
|
}
|
|
749
|
+
function hasExternalNotificationChannel(channels) {
|
|
750
|
+
return channels.some((channel) => channel?.type && channel.type !== 'openclaw-chat');
|
|
751
|
+
}
|
|
752
|
+
function hasSuccessfulExternalDelivery(results) {
|
|
753
|
+
return results.some((result) => result?.sent === true && result?.external === true);
|
|
754
|
+
}
|
|
669
755
|
async function deliverConnectorHealthAlert({ config, configPath, message, statusPayload, unhealthyConnectors, fingerprint }) {
|
|
670
756
|
const channels = getConnectorHealthChannels(config);
|
|
671
757
|
if (config?.notifications?.connectorHealth?.enabled === false) {
|
|
@@ -701,48 +787,23 @@ async function deliverConnectorHealthAlert({ config, configPath, message, status
|
|
|
701
787
|
});
|
|
702
788
|
}
|
|
703
789
|
}
|
|
790
|
+
if (!hasSuccessfulExternalDelivery(results)) {
|
|
791
|
+
results.push({
|
|
792
|
+
sent: false,
|
|
793
|
+
external: true,
|
|
794
|
+
target: 'external_notification',
|
|
795
|
+
detail: hasExternalNotificationChannel(channels)
|
|
796
|
+
? 'No external notification channel successfully sent the alert.'
|
|
797
|
+
: 'Alert written locally, but no external notification channel configured.',
|
|
798
|
+
});
|
|
799
|
+
}
|
|
704
800
|
return results;
|
|
705
801
|
}
|
|
706
802
|
function getGrowthRunChannels(config) {
|
|
707
803
|
const configuredChannels = Array.isArray(config?.notifications?.growthRun?.channels)
|
|
708
|
-
? config.notifications.growthRun.channels
|
|
804
|
+
? config.notifications.growthRun.channels
|
|
709
805
|
: [];
|
|
710
|
-
|
|
711
|
-
return configuredChannels;
|
|
712
|
-
const channels = [];
|
|
713
|
-
const deliveries = config?.deliveries || {};
|
|
714
|
-
if (deliveries.openclawChat?.enabled) {
|
|
715
|
-
channels.push({
|
|
716
|
-
type: 'openclaw-chat',
|
|
717
|
-
label: 'openclaw_chat',
|
|
718
|
-
markdownPath: deliveries.openclawChat.growthRunMarkdownPath || '.openclaw/chat/growth-summary.md',
|
|
719
|
-
jsonPath: deliveries.openclawChat.growthRunJsonPath || '.openclaw/chat/growth-summary.json',
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
if (deliveries.slack?.enabled) {
|
|
723
|
-
channels.push({
|
|
724
|
-
type: 'slack',
|
|
725
|
-
label: 'slack',
|
|
726
|
-
webhookEnv: deliveries.slack.webhookEnv || 'SLACK_WEBHOOK_URL',
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
if (deliveries.webhook?.enabled) {
|
|
730
|
-
channels.push({
|
|
731
|
-
type: 'webhook',
|
|
732
|
-
label: 'webhook',
|
|
733
|
-
urlEnv: deliveries.webhook.urlEnv || 'OPENCLAW_WEBHOOK_URL',
|
|
734
|
-
method: deliveries.webhook.method || 'POST',
|
|
735
|
-
headers: deliveries.webhook.headers || {},
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
if (deliveries.discord?.enabled) {
|
|
739
|
-
channels.push({
|
|
740
|
-
type: 'command',
|
|
741
|
-
label: 'discord',
|
|
742
|
-
command: deliveries.discord.command || 'node scripts/discord-openclaw-bridge.mjs send --stdin',
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
return channels;
|
|
806
|
+
return mergeNotificationChannelsWithDeliveries(configuredChannels, getDeliveryNotificationChannels(config, 'growthRun'));
|
|
746
807
|
}
|
|
747
808
|
async function readChartAttachments(chartManifestPath) {
|
|
748
809
|
if (!chartManifestPath)
|
|
@@ -800,9 +861,8 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
|
|
|
800
861
|
return `${lines.join('\n')}\n`;
|
|
801
862
|
}
|
|
802
863
|
async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, message, issuesPayload, activeCadences, fingerprint, charts) {
|
|
803
|
-
const
|
|
804
|
-
const
|
|
805
|
-
const jsonPath = path.resolve(baseDir, channel.jsonPath || '.openclaw/chat/growth-summary.json');
|
|
864
|
+
const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/growth-summary.md');
|
|
865
|
+
const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/growth-summary.json');
|
|
806
866
|
await fs.mkdir(path.dirname(markdownPath), { recursive: true });
|
|
807
867
|
await fs.mkdir(path.dirname(jsonPath), { recursive: true });
|
|
808
868
|
await fs.writeFile(markdownPath, message, 'utf8');
|
|
@@ -822,8 +882,9 @@ async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, mes
|
|
|
822
882
|
}, null, 2), 'utf8');
|
|
823
883
|
return {
|
|
824
884
|
sent: true,
|
|
885
|
+
external: false,
|
|
825
886
|
target: channel.label || 'openclaw_chat',
|
|
826
|
-
detail: `wrote ${markdownPath} and ${jsonPath}`,
|
|
887
|
+
detail: `wrote local OpenClaw chat outbox ${markdownPath} and ${jsonPath}`,
|
|
827
888
|
};
|
|
828
889
|
}
|
|
829
890
|
async function sendSlackGrowthSummary(channel, message) {
|
|
@@ -839,6 +900,7 @@ async function sendSlackGrowthSummary(channel, message) {
|
|
|
839
900
|
});
|
|
840
901
|
return {
|
|
841
902
|
sent: response.ok,
|
|
903
|
+
external: true,
|
|
842
904
|
target: channel.label || 'slack',
|
|
843
905
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
844
906
|
};
|
|
@@ -873,6 +935,7 @@ async function sendWebhookGrowthSummary(channel, message, issuesPayload, activeC
|
|
|
873
935
|
});
|
|
874
936
|
return {
|
|
875
937
|
sent: response.ok,
|
|
938
|
+
external: true,
|
|
876
939
|
target: channel.label || 'webhook',
|
|
877
940
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
878
941
|
};
|
|
@@ -884,6 +947,7 @@ async function sendCommandGrowthSummary(channel, message) {
|
|
|
884
947
|
const result = await runShellCommand(String(channel.command), 60_000, { input: message });
|
|
885
948
|
return {
|
|
886
949
|
sent: result.ok,
|
|
950
|
+
external: true,
|
|
887
951
|
target: channel.label || 'command',
|
|
888
952
|
detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
889
953
|
};
|
|
@@ -937,6 +1001,12 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
937
1001
|
const healthState = state?.connectorHealth || {};
|
|
938
1002
|
const intervalMinutes = getConnectorHealthIntervalMinutes(config);
|
|
939
1003
|
if (!isDue(healthState.lastCheckedAt, intervalMinutes)) {
|
|
1004
|
+
await appendSchedulerProof('connector_health_not_due', {
|
|
1005
|
+
configPath,
|
|
1006
|
+
statePath,
|
|
1007
|
+
intervalMinutes,
|
|
1008
|
+
lastCheckedAt: healthState.lastCheckedAt || null,
|
|
1009
|
+
});
|
|
940
1010
|
return state;
|
|
941
1011
|
}
|
|
942
1012
|
await ensureDir(runtimeDir);
|
|
@@ -962,6 +1032,13 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
962
1032
|
};
|
|
963
1033
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
964
1034
|
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1035
|
+
await appendSchedulerProof('connector_health_check_failed', {
|
|
1036
|
+
configPath,
|
|
1037
|
+
statePath,
|
|
1038
|
+
intervalMinutes,
|
|
1039
|
+
checkedAt,
|
|
1040
|
+
error: nextState.connectorHealth.lastError,
|
|
1041
|
+
});
|
|
965
1042
|
return nextState;
|
|
966
1043
|
}
|
|
967
1044
|
const unhealthyConnectors = getUnhealthyConfiguredConnectors(statusPayload);
|
|
@@ -975,11 +1052,10 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
975
1052
|
connectedConnectors,
|
|
976
1053
|
lastError: null,
|
|
977
1054
|
};
|
|
978
|
-
const
|
|
979
|
-
? healthState.activeIncidentFingerprint || healthState.lastAlertedFingerprint || null
|
|
980
|
-
: null;
|
|
1055
|
+
const previousExternallyDeliveredFingerprint = healthState.lastExternalAlertedFingerprint || null;
|
|
981
1056
|
if (unhealthyConnectors.length === 0) {
|
|
982
1057
|
nextHealthState.activeIncidentFingerprint = null;
|
|
1058
|
+
nextHealthState.lastExternalAlertedFingerprint = null;
|
|
983
1059
|
if (healthState.lastStatusOk === false) {
|
|
984
1060
|
nextHealthState.lastRecoveredAt = checkedAt;
|
|
985
1061
|
}
|
|
@@ -988,7 +1064,7 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
988
1064
|
nextHealthState.activeIncidentFingerprint = fingerprint;
|
|
989
1065
|
}
|
|
990
1066
|
if (unhealthyConnectors.length > 0 &&
|
|
991
|
-
|
|
1067
|
+
previousExternallyDeliveredFingerprint !== fingerprint) {
|
|
992
1068
|
const message = buildConnectorHealthAlert(statusPayload, unhealthyConnectors);
|
|
993
1069
|
const paths = await writeConnectorHealthAlert(runtimeDir, message, statusPayload, unhealthyConnectors, fingerprint);
|
|
994
1070
|
const deliveries = await deliverConnectorHealthAlert({
|
|
@@ -1004,6 +1080,11 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
1004
1080
|
nextHealthState.lastAlertMarkdownPath = paths.markdownPath;
|
|
1005
1081
|
nextHealthState.lastAlertJsonPath = paths.jsonPath;
|
|
1006
1082
|
nextHealthState.lastAlertDeliveries = deliveries;
|
|
1083
|
+
nextHealthState.lastAlertExternalSent = hasSuccessfulExternalDelivery(deliveries);
|
|
1084
|
+
if (nextHealthState.lastAlertExternalSent) {
|
|
1085
|
+
nextHealthState.lastExternalAlertedAt = checkedAt;
|
|
1086
|
+
nextHealthState.lastExternalAlertedFingerprint = fingerprint;
|
|
1087
|
+
}
|
|
1007
1088
|
}
|
|
1008
1089
|
const nextState = {
|
|
1009
1090
|
...state,
|
|
@@ -1011,6 +1092,22 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
1011
1092
|
};
|
|
1012
1093
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1013
1094
|
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1095
|
+
await appendSchedulerProof('connector_health_checked', {
|
|
1096
|
+
configPath,
|
|
1097
|
+
statePath,
|
|
1098
|
+
intervalMinutes,
|
|
1099
|
+
checkedAt,
|
|
1100
|
+
lastStatusOk: nextHealthState.lastStatusOk,
|
|
1101
|
+
connectedConnectors,
|
|
1102
|
+
unhealthyConnectors: unhealthyConnectors.map((entry) => ({
|
|
1103
|
+
key: entry.key,
|
|
1104
|
+
status: entry.status,
|
|
1105
|
+
detail: entry.detail,
|
|
1106
|
+
})),
|
|
1107
|
+
alertMarkdownPath: nextHealthState.lastAlertMarkdownPath || null,
|
|
1108
|
+
deliveryCount: Array.isArray(nextHealthState.lastAlertDeliveries) ? nextHealthState.lastAlertDeliveries.length : 0,
|
|
1109
|
+
externalDeliverySent: nextHealthState.lastAlertExternalSent === true,
|
|
1110
|
+
});
|
|
1014
1111
|
return nextState;
|
|
1015
1112
|
}
|
|
1016
1113
|
function buildIssueFingerprint(issuesPayload) {
|
|
@@ -1159,7 +1256,7 @@ function resolveCursorAwareCommand(command, sourceConfig, cursorState) {
|
|
|
1159
1256
|
const lookback = normalizeLookback(sourceConfig?.initialLookback, '30d');
|
|
1160
1257
|
return `${rawCommand} --last ${quote(lookback)}`;
|
|
1161
1258
|
}
|
|
1162
|
-
async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorState, commandCwd = process.cwd()) {
|
|
1259
|
+
async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorState, commandCwd = process.cwd(), configPath = null) {
|
|
1163
1260
|
if (!sourceConfig || sourceConfig.enabled === false) {
|
|
1164
1261
|
return {
|
|
1165
1262
|
payload: null,
|
|
@@ -1171,7 +1268,7 @@ async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorSt
|
|
|
1171
1268
|
if (!sourceConfig.command) {
|
|
1172
1269
|
throw new Error(`Source "${sourceName}" has mode=command but no command configured.`);
|
|
1173
1270
|
}
|
|
1174
|
-
const resolvedCommand = resolveCursorAwareCommand(replaceLegacyRuntimeScriptCommand(sourceConfig.command), sourceConfig, cursorState);
|
|
1271
|
+
const resolvedCommand = resolveCursorAwareCommand(withActiveConfigArg(replaceLegacyRuntimeScriptCommand(sourceConfig.command), configPath), sourceConfig, cursorState);
|
|
1175
1272
|
const result = await runShellCommand(String(resolvedCommand), 120_000, { cwd: commandCwd });
|
|
1176
1273
|
if (!result.ok) {
|
|
1177
1274
|
throw new Error(`Source "${sourceName}" command failed: ${result.stderr || `exit ${result.code}`}`);
|
|
@@ -1203,13 +1300,13 @@ async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorSt
|
|
|
1203
1300
|
resolvedCommand: null,
|
|
1204
1301
|
};
|
|
1205
1302
|
}
|
|
1206
|
-
async function loadSourcePayloads(config, state) {
|
|
1303
|
+
async function loadSourcePayloads(config, state, configPath) {
|
|
1207
1304
|
const payloads = {};
|
|
1208
1305
|
const sourceCursors = { ...(state?.sourceCursors || {}) };
|
|
1209
1306
|
const commandCwd = getProjectCommandCwd(config);
|
|
1210
1307
|
for (const source of getAllSourceEntries(config)) {
|
|
1211
1308
|
const currentCursor = sourceCursors[source.key] || null;
|
|
1212
|
-
const result = await resolveSourcePayloadWithCursor(source, source.key, currentCursor, commandCwd);
|
|
1309
|
+
const result = await resolveSourcePayloadWithCursor(source, source.key, currentCursor, commandCwd, configPath);
|
|
1213
1310
|
const payload = result.payload;
|
|
1214
1311
|
if (payload) {
|
|
1215
1312
|
payloads[source.key] = payload;
|
|
@@ -1247,6 +1344,11 @@ function hasSourceChanges(previousHashes, currentHashes) {
|
|
|
1247
1344
|
return false;
|
|
1248
1345
|
}
|
|
1249
1346
|
async function runOnce(configPath, statePath) {
|
|
1347
|
+
await appendSchedulerProof('runner_invoked', {
|
|
1348
|
+
configPath,
|
|
1349
|
+
statePath,
|
|
1350
|
+
argv: process.argv.slice(2),
|
|
1351
|
+
});
|
|
1250
1352
|
const config = await readJson(configPath);
|
|
1251
1353
|
await applyOpenClawSecretRefs(config);
|
|
1252
1354
|
const inferredGitHubRepo = await inferGitHubRepo(config);
|
|
@@ -1263,7 +1365,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1263
1365
|
lastRunAt: null,
|
|
1264
1366
|
sourceCursors: {},
|
|
1265
1367
|
});
|
|
1266
|
-
const runtimeDir = path.resolve(
|
|
1368
|
+
const runtimeDir = path.resolve(deriveRuntimeDirFromStatePath(statePath));
|
|
1267
1369
|
const stateAfterHealthCheck = await maybeRunConnectorHealthCheck({
|
|
1268
1370
|
config,
|
|
1269
1371
|
configPath,
|
|
@@ -1272,19 +1374,27 @@ async function runOnce(configPath, statePath) {
|
|
|
1272
1374
|
runtimeDir,
|
|
1273
1375
|
});
|
|
1274
1376
|
const activeCadences = getDueCadences(config, stateAfterHealthCheck);
|
|
1275
|
-
const { payloads, sourceCursors } = await loadSourcePayloads(config, stateAfterHealthCheck);
|
|
1377
|
+
const { payloads, sourceCursors } = await loadSourcePayloads(config, stateAfterHealthCheck, configPath);
|
|
1276
1378
|
const currentHashes = computeSourceHashes(payloads);
|
|
1277
1379
|
const changed = hasSourceChanges(stateAfterHealthCheck.sourceHashes, currentHashes);
|
|
1278
1380
|
if (!changed && config.schedule?.skipIfNoDataChange !== false) {
|
|
1279
1381
|
process.stdout.write(`[${new Date().toISOString()}] No data changes. Skip run.\n`);
|
|
1382
|
+
const completedAt = new Date().toISOString();
|
|
1280
1383
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1281
1384
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1282
1385
|
...stateAfterHealthCheck,
|
|
1283
1386
|
sourceHashes: currentHashes,
|
|
1284
1387
|
sourceCursors,
|
|
1285
|
-
lastRunAt:
|
|
1388
|
+
lastRunAt: completedAt,
|
|
1286
1389
|
skippedReason: 'no_data_change',
|
|
1287
1390
|
}, null, 2), 'utf8');
|
|
1391
|
+
await appendSchedulerProof('runner_completed', {
|
|
1392
|
+
configPath,
|
|
1393
|
+
statePath,
|
|
1394
|
+
completedAt,
|
|
1395
|
+
skippedReason: 'no_data_change',
|
|
1396
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1397
|
+
});
|
|
1288
1398
|
return;
|
|
1289
1399
|
}
|
|
1290
1400
|
const githubArtifactModes = getGitHubArtifactModes(config).filter((mode) => shouldAutoCreateGitHubArtifact(config, mode));
|
|
@@ -1312,17 +1422,26 @@ async function runOnce(configPath, statePath) {
|
|
|
1312
1422
|
const unchangedIssueSet = issueFingerprint === stateAfterHealthCheck.lastIssueFingerprint;
|
|
1313
1423
|
if (unchangedIssueSet && config.schedule?.skipIfIssueSetUnchanged !== false) {
|
|
1314
1424
|
process.stdout.write(`[${new Date().toISOString()}] Issue set unchanged. Skip GitHub creation.\n`);
|
|
1425
|
+
const completedAt = new Date().toISOString();
|
|
1315
1426
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1316
1427
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1317
1428
|
...stateAfterHealthCheck,
|
|
1318
1429
|
sourceHashes: currentHashes,
|
|
1319
1430
|
sourceCursors,
|
|
1320
1431
|
lastIssueFingerprint: issueFingerprint,
|
|
1321
|
-
lastRunAt:
|
|
1432
|
+
lastRunAt: completedAt,
|
|
1322
1433
|
lastOutFile: dryRun.outFile,
|
|
1323
|
-
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences,
|
|
1434
|
+
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences, completedAt),
|
|
1324
1435
|
skippedReason: 'issue_set_unchanged',
|
|
1325
1436
|
}, null, 2), 'utf8');
|
|
1437
|
+
await appendSchedulerProof('runner_completed', {
|
|
1438
|
+
configPath,
|
|
1439
|
+
statePath,
|
|
1440
|
+
completedAt,
|
|
1441
|
+
skippedReason: 'issue_set_unchanged',
|
|
1442
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1443
|
+
outFile: dryRun.outFile,
|
|
1444
|
+
});
|
|
1326
1445
|
return;
|
|
1327
1446
|
}
|
|
1328
1447
|
const shouldCreateGitHubArtifact = createGitHubArtifact && Number(dryRun.issuesPayload?.issue_count || 0) > 0;
|
|
@@ -1343,15 +1462,16 @@ async function runOnce(configPath, statePath) {
|
|
|
1343
1462
|
else {
|
|
1344
1463
|
process.stdout.write(`[${new Date().toISOString()}] Drafts generated only (${getActionMode(config)} auto-create disabled).\n`);
|
|
1345
1464
|
}
|
|
1465
|
+
const completedAt = new Date().toISOString();
|
|
1346
1466
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1347
1467
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1348
1468
|
...stateAfterHealthCheck,
|
|
1349
1469
|
sourceHashes: currentHashes,
|
|
1350
1470
|
sourceCursors,
|
|
1351
1471
|
lastIssueFingerprint: issueFingerprint,
|
|
1352
|
-
lastRunAt:
|
|
1472
|
+
lastRunAt: completedAt,
|
|
1353
1473
|
lastOutFile: dryRun.outFile,
|
|
1354
|
-
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences,
|
|
1474
|
+
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences, completedAt),
|
|
1355
1475
|
lastGrowthRunNotifications: await deliverGrowthRunSummary({
|
|
1356
1476
|
config,
|
|
1357
1477
|
configPath,
|
|
@@ -1364,6 +1484,16 @@ async function runOnce(configPath, statePath) {
|
|
|
1364
1484
|
}),
|
|
1365
1485
|
skippedReason: null,
|
|
1366
1486
|
}, null, 2), 'utf8');
|
|
1487
|
+
await appendSchedulerProof('runner_completed', {
|
|
1488
|
+
configPath,
|
|
1489
|
+
statePath,
|
|
1490
|
+
completedAt,
|
|
1491
|
+
skippedReason: null,
|
|
1492
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1493
|
+
outFile: dryRun.outFile,
|
|
1494
|
+
issueCount: Number(dryRun.issuesPayload?.issue_count || 0),
|
|
1495
|
+
createdGitHubArtifact: shouldCreateGitHubArtifact,
|
|
1496
|
+
});
|
|
1367
1497
|
}
|
|
1368
1498
|
async function main() {
|
|
1369
1499
|
await loadOpenClawGrowthSecrets();
|
|
@@ -1371,6 +1501,7 @@ async function main() {
|
|
|
1371
1501
|
await maybeSelfUpdateFromClawHub(args);
|
|
1372
1502
|
const configPath = path.resolve(args.config);
|
|
1373
1503
|
const statePath = path.resolve(args.state);
|
|
1504
|
+
useSchedulerProofPathForStatePath(statePath);
|
|
1374
1505
|
if (!args.loop) {
|
|
1375
1506
|
await runOnce(configPath, statePath);
|
|
1376
1507
|
return;
|
|
@@ -1384,12 +1515,21 @@ async function main() {
|
|
|
1384
1515
|
await runOnce(configPath, statePath);
|
|
1385
1516
|
}
|
|
1386
1517
|
catch (error) {
|
|
1518
|
+
await appendSchedulerProof('runner_failed', {
|
|
1519
|
+
configPath,
|
|
1520
|
+
statePath,
|
|
1521
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1522
|
+
}).catch(() => { });
|
|
1387
1523
|
process.stderr.write(`[${new Date().toISOString()}] Run failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1388
1524
|
}
|
|
1389
1525
|
await sleep(intervalMinutes * 60_000);
|
|
1390
1526
|
}
|
|
1391
1527
|
}
|
|
1392
|
-
main().catch((error) => {
|
|
1528
|
+
main().catch(async (error) => {
|
|
1529
|
+
await appendSchedulerProof('runner_failed', {
|
|
1530
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1531
|
+
argv: process.argv.slice(2),
|
|
1532
|
+
}).catch(() => { });
|
|
1393
1533
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1394
1534
|
process.exitCode = 1;
|
|
1395
1535
|
});
|