thumbgate 1.3.0 → 1.4.1
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/.claude-plugin/README.md +25 -0
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +204 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +54 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/operational-summary.js +41 -5
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -13,15 +13,13 @@ const fs = require('fs');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { execSync } = require('child_process');
|
|
16
|
+
const { ensureDir } = require('./fs-utils');
|
|
16
17
|
|
|
17
18
|
const ROOT = path.join(__dirname, '..');
|
|
18
19
|
function getProofDir() {
|
|
19
20
|
return process.env.THUMBGATE_PROOF_DIR || path.join(ROOT, 'proof');
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function ensureDir(d) {
|
|
23
|
-
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
24
|
-
}
|
|
25
23
|
|
|
26
24
|
// ---------------------------------------------------------------------------
|
|
27
25
|
// Run test suite and parse results
|
package/scripts/prove-lancedb.js
CHANGED
|
@@ -17,6 +17,7 @@ const path = require('path');
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
const { escapeMarkdownTableCell } = require('./markdown-escape');
|
|
20
|
+
const { ensureDir } = require('./fs-utils');
|
|
20
21
|
|
|
21
22
|
const ROOT = path.join(__dirname, '..');
|
|
22
23
|
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
|
|
@@ -63,11 +64,6 @@ function createInMemoryLanceLoader() {
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function ensureDir(dirPath) {
|
|
67
|
-
if (!fs.existsSync(dirPath)) {
|
|
68
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
67
|
|
|
72
68
|
function status(condition) {
|
|
73
69
|
return condition ? 'pass' : 'fail';
|
|
@@ -5,13 +5,11 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
+
const { ensureDir } = require('./fs-utils');
|
|
8
9
|
|
|
9
10
|
const ROOT = path.join(__dirname, '..');
|
|
10
11
|
const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR || path.join(ROOT, 'proof');
|
|
11
12
|
|
|
12
|
-
function ensureDir(dirPath) {
|
|
13
|
-
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
-
}
|
|
15
13
|
|
|
16
14
|
function runTests() {
|
|
17
15
|
try {
|
|
@@ -10,6 +10,9 @@ const { execFileSync } = require('child_process');
|
|
|
10
10
|
|
|
11
11
|
const ROOT = path.join(__dirname, '..');
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
13
|
+
const DEFAULT_PUBLISH_INSTALL_RETRIES = 6;
|
|
14
|
+
const DEFAULT_PUBLISH_INSTALL_DELAY_MS = 5000;
|
|
15
|
+
const MAX_PUBLISH_INSTALL_DELAY_MS = 30000;
|
|
13
16
|
const STATUSLINE_INPUT = JSON.stringify({ context_window: { used_percentage: 12 } });
|
|
14
17
|
|
|
15
18
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
@@ -55,6 +58,53 @@ function installPackage(prefixDir, packageSpec) {
|
|
|
55
58
|
return path.join(prefixDir, 'node_modules', '.bin', 'thumbgate');
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
function isRemotePackageSpec(packageSpec) {
|
|
62
|
+
if (!packageSpec) return false;
|
|
63
|
+
return !/^(?:\.{0,2}\/|\/|file:)/.test(packageSpec) && !packageSpec.endsWith('.tgz');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isTransientRegistryMiss(error) {
|
|
67
|
+
const text = [
|
|
68
|
+
error && error.message,
|
|
69
|
+
error && error.stdout,
|
|
70
|
+
error && error.stderr,
|
|
71
|
+
]
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join('\n');
|
|
74
|
+
return /ETARGET|No matching version found|npm error code E404|404 Not Found/i.test(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function installPackageWithRetry(prefixDir, packageSpec, options = {}) {
|
|
78
|
+
const installImpl = options.installImpl || installPackage;
|
|
79
|
+
const sleepImpl = options.sleepImpl || sleep;
|
|
80
|
+
const remotePackage = options.remotePackage !== undefined ? options.remotePackage : isRemotePackageSpec(packageSpec);
|
|
81
|
+
const attempts = remotePackage ? Number(options.attempts || DEFAULT_PUBLISH_INSTALL_RETRIES) : 1;
|
|
82
|
+
let delayMs = Number(options.delayMs || DEFAULT_PUBLISH_INSTALL_DELAY_MS);
|
|
83
|
+
let lastError = null;
|
|
84
|
+
|
|
85
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
86
|
+
if (attempt > 1) {
|
|
87
|
+
fs.rmSync(prefixDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return installImpl(prefixDir, packageSpec);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
lastError = error;
|
|
93
|
+
const retryable = remotePackage && isTransientRegistryMiss(error) && attempt < attempts;
|
|
94
|
+
if (!retryable) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`Retrying published package install for ${packageSpec} after transient registry miss (${attempt}/${attempts - 1})\n`
|
|
99
|
+
);
|
|
100
|
+
await sleepImpl(delayMs);
|
|
101
|
+
delayMs = Math.min(Math.round(delayMs * 1.5), MAX_PUBLISH_INSTALL_DELAY_MS);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw lastError || new Error(`Failed to install package ${packageSpec}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
function request(url, timeoutMs = 2000) {
|
|
59
109
|
return new Promise((resolve, reject) => {
|
|
60
110
|
const req = http.get(url, (res) => {
|
|
@@ -175,7 +225,10 @@ async function runPackagedRuntimeSmoke(options = {}) {
|
|
|
175
225
|
|
|
176
226
|
try {
|
|
177
227
|
const packageSpec = options.packageSpec || packCurrentRepo(packDir);
|
|
178
|
-
const runtimeBin =
|
|
228
|
+
const runtimeBin = await installPackageWithRetry(runtimeDir, packageSpec, {
|
|
229
|
+
attempts: options.installAttempts,
|
|
230
|
+
delayMs: options.installDelayMs,
|
|
231
|
+
});
|
|
179
232
|
if (!fs.existsSync(runtimeBin)) {
|
|
180
233
|
throw new Error(`Installed runtime binary is missing: ${runtimeBin}`);
|
|
181
234
|
}
|
|
@@ -210,17 +263,25 @@ async function runPackagedRuntimeSmoke(options = {}) {
|
|
|
210
263
|
}
|
|
211
264
|
|
|
212
265
|
const readyStatusline = renderStatusline(runtimeBin, projectDir, env);
|
|
213
|
-
if (
|
|
214
|
-
throw new Error(`Ready statusline missing dashboard
|
|
266
|
+
if (!/(Dashboard|Dashboard…)/.test(readyStatusline)) {
|
|
267
|
+
throw new Error(`Ready statusline missing dashboard label: ${readyStatusline.trim()}`);
|
|
268
|
+
}
|
|
269
|
+
if (!/(Lessons|Lessons…)/.test(readyStatusline)) {
|
|
270
|
+
throw new Error(`Ready statusline missing lessons label: ${readyStatusline.trim()}`);
|
|
271
|
+
}
|
|
272
|
+
if (readyStatusline.includes(`${origin}/dashboard`)) {
|
|
273
|
+
throw new Error(`Ready statusline leaked dashboard URL: ${readyStatusline.trim()}`);
|
|
215
274
|
}
|
|
216
|
-
if (
|
|
217
|
-
throw new Error(`Ready statusline
|
|
275
|
+
if (readyStatusline.includes(`${origin}/lessons`)) {
|
|
276
|
+
throw new Error(`Ready statusline leaked lessons URL: ${readyStatusline.trim()}`);
|
|
218
277
|
}
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
// Thumbs-up/down icons stay inline while dashboard + lessons remain compact
|
|
279
|
+
// labels, even after the local API is up.
|
|
280
|
+
if (!readyStatusline.includes('👍')) {
|
|
281
|
+
throw new Error(`Ready statusline missing thumbs-up icon: ${readyStatusline.trim()}`);
|
|
221
282
|
}
|
|
222
|
-
if (!readyStatusline.includes(
|
|
223
|
-
throw new Error(`Ready statusline missing thumbs-down
|
|
283
|
+
if (!readyStatusline.includes('👎')) {
|
|
284
|
+
throw new Error(`Ready statusline missing thumbs-down icon: ${readyStatusline.trim()}`);
|
|
224
285
|
}
|
|
225
286
|
|
|
226
287
|
return {
|
|
@@ -241,6 +302,8 @@ async function main() {
|
|
|
241
302
|
packageSpec: args.packageSpec,
|
|
242
303
|
expectedVersion: args.expectedVersion,
|
|
243
304
|
timeoutMs: args.timeoutMs,
|
|
305
|
+
installAttempts: args.installAttempts,
|
|
306
|
+
installDelayMs: args.installDelayMs,
|
|
244
307
|
});
|
|
245
308
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
246
309
|
}
|
|
@@ -254,6 +317,9 @@ if (require.main === module) {
|
|
|
254
317
|
|
|
255
318
|
module.exports = {
|
|
256
319
|
getAvailablePort,
|
|
320
|
+
installPackageWithRetry,
|
|
321
|
+
isRemotePackageSpec,
|
|
322
|
+
isTransientRegistryMiss,
|
|
257
323
|
packCurrentRepo,
|
|
258
324
|
runPackagedRuntimeSmoke,
|
|
259
325
|
waitForHealthy,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
+
const { ensureDir } = require('./fs-utils');
|
|
7
8
|
|
|
8
9
|
const ROOT = path.join(__dirname, '..');
|
|
9
10
|
|
|
@@ -16,9 +17,6 @@ function resolveProofPaths() {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function ensureDir(dirPath) {
|
|
20
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
21
|
-
}
|
|
22
20
|
|
|
23
21
|
function buildStubTelemetry() {
|
|
24
22
|
return {
|
|
@@ -13,15 +13,13 @@ const fs = require('fs');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { execSync } = require('child_process');
|
|
16
|
+
const { ensureDir } = require('./fs-utils');
|
|
16
17
|
|
|
17
18
|
const ROOT = path.join(__dirname, '..');
|
|
18
19
|
function getProofDir() {
|
|
19
20
|
return process.env.THUMBGATE_PROOF_DIR || path.join(ROOT, 'proof');
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function ensureDir(d) {
|
|
23
|
-
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
24
|
-
}
|
|
25
23
|
|
|
26
24
|
function runTests() {
|
|
27
25
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
+
const { ensureDir } = require('./fs-utils');
|
|
4
5
|
const {
|
|
5
6
|
runWorkflowContractValidation,
|
|
6
7
|
} = require('./validate-workflow-contract');
|
|
@@ -8,11 +9,6 @@ const {
|
|
|
8
9
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
9
10
|
const DEFAULT_PROOF_DIR = path.join(PROJECT_ROOT, 'proof', 'workflow-contract');
|
|
10
11
|
|
|
11
|
-
function ensureDir(dirPath) {
|
|
12
|
-
if (!fs.existsSync(dirPath)) {
|
|
13
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
12
|
|
|
17
13
|
function toMarkdown(report) {
|
|
18
14
|
const lines = [
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const {
|
|
8
|
+
buildEngagementAudit,
|
|
9
|
+
DEFAULT_DRAFTS_PATH,
|
|
10
|
+
DEFAULT_LAUNCH_ASSETS_PATH,
|
|
11
|
+
DEFAULT_REPLY_STATE_PATH,
|
|
12
|
+
DEFAULT_TIMEZONE,
|
|
13
|
+
} = require('./social-analytics/engagement-audit');
|
|
14
|
+
|
|
15
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
16
|
+
const DEFAULT_ARTIFACT_DIR = path.join(REPO_ROOT, '.artifacts', 'ralph-loop');
|
|
17
|
+
const VALID_MODES = new Set(['all', 'engage', 'poll', 'audit', 'post']);
|
|
18
|
+
const RALPH_STATE_PATHS = [
|
|
19
|
+
path.relative(REPO_ROOT, DEFAULT_REPLY_STATE_PATH),
|
|
20
|
+
path.relative(REPO_ROOT, DEFAULT_DRAFTS_PATH),
|
|
21
|
+
path.relative(REPO_ROOT, DEFAULT_LAUNCH_ASSETS_PATH),
|
|
22
|
+
];
|
|
23
|
+
const VALUE_OPTIONS = new Map([
|
|
24
|
+
['--artifact-dir', 'artifactDir'],
|
|
25
|
+
['--date', 'date'],
|
|
26
|
+
['--mode', 'mode'],
|
|
27
|
+
['--timezone', 'timezone'],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv = []) {
|
|
31
|
+
const options = {
|
|
32
|
+
artifactDir: DEFAULT_ARTIFACT_DIR,
|
|
33
|
+
date: '',
|
|
34
|
+
dryRun: false,
|
|
35
|
+
mode: 'all',
|
|
36
|
+
timezone: DEFAULT_TIMEZONE,
|
|
37
|
+
replyStatePath: DEFAULT_REPLY_STATE_PATH,
|
|
38
|
+
draftsPath: DEFAULT_DRAFTS_PATH,
|
|
39
|
+
launchAssetsPath: DEFAULT_LAUNCH_ASSETS_PATH,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
index = consumeArg(options, argv, index);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
options.mode = normalizeMode(options.mode);
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function consumeArg(options, argv, index) {
|
|
51
|
+
const token = String(argv[index] || '').trim();
|
|
52
|
+
if (token === '--dry-run') {
|
|
53
|
+
options.dryRun = true;
|
|
54
|
+
return index;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const inline = token.match(/^(--[^=]+)=(.*)$/);
|
|
58
|
+
if (inline && VALUE_OPTIONS.has(inline[1])) {
|
|
59
|
+
setOption(options, VALUE_OPTIONS.get(inline[1]), inline[2]);
|
|
60
|
+
return index;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (VALUE_OPTIONS.has(token) && argv[index + 1]) {
|
|
64
|
+
setOption(options, VALUE_OPTIONS.get(token), argv[index + 1]);
|
|
65
|
+
return index + 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return index;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setOption(options, name, value) {
|
|
72
|
+
const trimmed = String(value || '').trim();
|
|
73
|
+
if (name === 'artifactDir') {
|
|
74
|
+
options.artifactDir = path.resolve(trimmed || DEFAULT_ARTIFACT_DIR);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (name === 'timezone') {
|
|
78
|
+
options.timezone = trimmed || DEFAULT_TIMEZONE;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
options[name] = trimmed || options[name];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeMode(mode) {
|
|
85
|
+
const normalized = String(mode || 'all').trim().toLowerCase();
|
|
86
|
+
if (!VALID_MODES.has(normalized)) {
|
|
87
|
+
throw new Error(`Invalid Ralph mode: ${mode}. Expected one of: ${[...VALID_MODES].join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasAnyEnv(env, keys = []) {
|
|
93
|
+
return keys.some((key) => Boolean(env[key]));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasAllEnv(env, keys = []) {
|
|
97
|
+
return keys.every((key) => Boolean(env[key]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function makeNodeStep(id, scriptPath, args = [], extra = {}) {
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
command: process.execPath,
|
|
104
|
+
args: [path.join(REPO_ROOT, scriptPath), ...args],
|
|
105
|
+
scriptPath,
|
|
106
|
+
type: 'node',
|
|
107
|
+
...extra,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function withSkipReason(step, env) {
|
|
112
|
+
if (step.requiredEnvAll && !hasAllEnv(env, step.requiredEnvAll)) {
|
|
113
|
+
return {
|
|
114
|
+
...step,
|
|
115
|
+
skipReason: `missing env: ${step.requiredEnvAll.filter((key) => !env[key]).join(', ')}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (step.requiredEnvAny && !hasAnyEnv(env, step.requiredEnvAny)) {
|
|
119
|
+
return {
|
|
120
|
+
...step,
|
|
121
|
+
skipReason: `missing one of: ${step.requiredEnvAny.join(', ')}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return step;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function wants(mode, names) {
|
|
128
|
+
return mode === 'all' || names.includes(mode);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildRalphSteps(options = {}, env = process.env) {
|
|
132
|
+
const mode = normalizeMode(options.mode || 'all');
|
|
133
|
+
const dryRun = Boolean(options.dryRun);
|
|
134
|
+
const steps = [];
|
|
135
|
+
|
|
136
|
+
if (wants(mode, ['poll'])) {
|
|
137
|
+
steps.push(makeNodeStep(
|
|
138
|
+
'poll-analytics',
|
|
139
|
+
'scripts/social-analytics/poll-all.js',
|
|
140
|
+
[],
|
|
141
|
+
{
|
|
142
|
+
stage: 'sense',
|
|
143
|
+
description: 'Polls configured social analytics for audience and attribution signals.',
|
|
144
|
+
}
|
|
145
|
+
));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (wants(mode, ['engage'])) {
|
|
149
|
+
steps.push(withSkipReason(makeNodeStep(
|
|
150
|
+
'sync-launch-assets',
|
|
151
|
+
'scripts/social-analytics/sync-launch-assets.js',
|
|
152
|
+
['--limit=50', `--state-path=${options.launchAssetsPath || DEFAULT_LAUNCH_ASSETS_PATH}`],
|
|
153
|
+
{
|
|
154
|
+
stage: 'sense',
|
|
155
|
+
description: 'Syncs owned Zernio launch assets so reply monitoring anchors on current campaign posts.',
|
|
156
|
+
requiredEnvAll: ['ZERNIO_API_KEY'],
|
|
157
|
+
}
|
|
158
|
+
), env));
|
|
159
|
+
|
|
160
|
+
const replyArgs = [];
|
|
161
|
+
if (dryRun) {
|
|
162
|
+
replyArgs.push('--dry-run');
|
|
163
|
+
}
|
|
164
|
+
steps.push(makeNodeStep(
|
|
165
|
+
'reply-monitor',
|
|
166
|
+
'scripts/social-reply-monitor.js',
|
|
167
|
+
replyArgs,
|
|
168
|
+
{
|
|
169
|
+
stage: 'engage',
|
|
170
|
+
description: 'Checks Reddit, X, and LinkedIn reply surfaces with platform-safe posting and draft rules.',
|
|
171
|
+
}
|
|
172
|
+
));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mode === 'post') {
|
|
176
|
+
const postArgs = [];
|
|
177
|
+
if (dryRun) {
|
|
178
|
+
postArgs.push('--dry-run');
|
|
179
|
+
}
|
|
180
|
+
steps.push(makeNodeStep(
|
|
181
|
+
'daily-social-post',
|
|
182
|
+
'scripts/social-post-hourly.js',
|
|
183
|
+
postArgs,
|
|
184
|
+
{
|
|
185
|
+
stage: 'publish',
|
|
186
|
+
description: 'Runs the one-quality-post lane on demand. Ralph hourly mode does not call this step.',
|
|
187
|
+
requiredEnvAll: ['ZERNIO_API_KEY'],
|
|
188
|
+
}
|
|
189
|
+
));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
steps.push({
|
|
193
|
+
id: 'engagement-audit',
|
|
194
|
+
stage: 'prove',
|
|
195
|
+
type: 'internal',
|
|
196
|
+
description: 'Builds a machine-readable Ralph Loop audit from reply state, drafts, and launch assets.',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return steps.map((step) => withSkipReason(step, env));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runExternalStep(step, env = process.env) {
|
|
203
|
+
const result = spawnSync(step.command, step.args, {
|
|
204
|
+
cwd: REPO_ROOT,
|
|
205
|
+
env,
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
212
|
+
stdout: result.stdout || '',
|
|
213
|
+
stderr: result.stderr || '',
|
|
214
|
+
error: result.error ? result.error.message : '',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runAuditStep(options = {}) {
|
|
219
|
+
return buildEngagementAudit({
|
|
220
|
+
date: options.date,
|
|
221
|
+
timezone: options.timezone || DEFAULT_TIMEZONE,
|
|
222
|
+
replyStatePath: options.replyStatePath || DEFAULT_REPLY_STATE_PATH,
|
|
223
|
+
draftsPath: options.draftsPath || DEFAULT_DRAFTS_PATH,
|
|
224
|
+
launchAssetsPath: options.launchAssetsPath || DEFAULT_LAUNCH_ASSETS_PATH,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function runStep(step, options = {}, deps = {}) {
|
|
229
|
+
const startedAt = new Date().toISOString();
|
|
230
|
+
|
|
231
|
+
if (step.skipReason) {
|
|
232
|
+
return {
|
|
233
|
+
id: step.id,
|
|
234
|
+
stage: step.stage,
|
|
235
|
+
status: 'skipped',
|
|
236
|
+
skipReason: step.skipReason,
|
|
237
|
+
startedAt,
|
|
238
|
+
finishedAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (step.type === 'internal') {
|
|
243
|
+
const audit = runAuditStep(options);
|
|
244
|
+
return {
|
|
245
|
+
id: step.id,
|
|
246
|
+
stage: step.stage,
|
|
247
|
+
status: 'passed',
|
|
248
|
+
audit,
|
|
249
|
+
startedAt,
|
|
250
|
+
finishedAt: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const runner = deps.runner || runExternalStep;
|
|
255
|
+
const result = runner(step, deps.env || process.env);
|
|
256
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
257
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
id: step.id,
|
|
261
|
+
stage: step.stage,
|
|
262
|
+
status: result.exitCode === 0 ? 'passed' : 'failed',
|
|
263
|
+
exitCode: result.exitCode,
|
|
264
|
+
error: result.error || '',
|
|
265
|
+
stdoutTail: tail(result.stdout),
|
|
266
|
+
stderrTail: tail(result.stderr),
|
|
267
|
+
startedAt,
|
|
268
|
+
finishedAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function tail(text, maxChars = 4000) {
|
|
273
|
+
const value = String(text || '');
|
|
274
|
+
return value.length <= maxChars ? value : value.slice(value.length - maxChars);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderMarkdownReport(report) {
|
|
278
|
+
const lines = [
|
|
279
|
+
'# Ralph Loop Audience Engagement Report',
|
|
280
|
+
'',
|
|
281
|
+
`Generated: ${report.generatedAt}`,
|
|
282
|
+
`Mode: ${report.mode}`,
|
|
283
|
+
`Dry run: ${report.dryRun ? 'yes' : 'no'}`,
|
|
284
|
+
'',
|
|
285
|
+
'Ralph Mode keeps the Reliability Gateway pointed at acquisition: sense audience signals, engage safely, and preserve proof for Pre-Action Gates, DPO, and Thompson Sampling review.',
|
|
286
|
+
'',
|
|
287
|
+
'## Steps',
|
|
288
|
+
'',
|
|
289
|
+
'| Step | Stage | Status | Evidence |',
|
|
290
|
+
'|------|-------|--------|----------|',
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
for (const step of report.steps) {
|
|
294
|
+
const evidence = step.skipReason || step.error || `exit ${step.exitCode ?? 0}`;
|
|
295
|
+
lines.push(`| ${step.id} | ${step.stage} | ${step.status} | ${String(evidence).replaceAll('|', '/')} |`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lines.push(
|
|
299
|
+
'',
|
|
300
|
+
'## Audit',
|
|
301
|
+
'',
|
|
302
|
+
`- Checked: ${report.audit.totals.checked}`,
|
|
303
|
+
`- Replied: ${report.audit.totals.replied}`,
|
|
304
|
+
`- Drafted: ${report.audit.totals.drafted}`,
|
|
305
|
+
`- Skipped: ${report.audit.totals.skipped}`,
|
|
306
|
+
'',
|
|
307
|
+
'Authority evidence: docs/VERIFICATION_EVIDENCE.md',
|
|
308
|
+
''
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return `${lines.join('\n')}\n`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeReports(report, artifactDir = DEFAULT_ARTIFACT_DIR) {
|
|
315
|
+
fs.mkdirSync(artifactDir, { recursive: true });
|
|
316
|
+
const jsonPath = path.join(artifactDir, 'ralph-loop-report.json');
|
|
317
|
+
const markdownPath = path.join(artifactDir, 'ralph-loop-report.md');
|
|
318
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
319
|
+
fs.writeFileSync(markdownPath, renderMarkdownReport(report), 'utf8');
|
|
320
|
+
return { jsonPath, markdownPath };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function runRalphLoop(options = {}, deps = {}) {
|
|
324
|
+
const normalized = {
|
|
325
|
+
...parseArgs([]),
|
|
326
|
+
...options,
|
|
327
|
+
mode: normalizeMode(options.mode || 'all'),
|
|
328
|
+
};
|
|
329
|
+
const env = deps.env || process.env;
|
|
330
|
+
const steps = buildRalphSteps(normalized, env);
|
|
331
|
+
const results = steps.map((step) => runStep(step, normalized, { ...deps, env }));
|
|
332
|
+
const auditStep = results.find((step) => step.id === 'engagement-audit');
|
|
333
|
+
const audit = auditStep?.audit ? auditStep.audit : runAuditStep(normalized);
|
|
334
|
+
const report = {
|
|
335
|
+
generatedAt: new Date().toISOString(),
|
|
336
|
+
mode: normalized.mode,
|
|
337
|
+
dryRun: Boolean(normalized.dryRun),
|
|
338
|
+
cadence: 'hourly_ci',
|
|
339
|
+
statePaths: RALPH_STATE_PATHS,
|
|
340
|
+
steps: results,
|
|
341
|
+
audit,
|
|
342
|
+
};
|
|
343
|
+
report.artifacts = writeReports(report, normalized.artifactDir || DEFAULT_ARTIFACT_DIR);
|
|
344
|
+
return report;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isCliEntrypoint(argv = process.argv) {
|
|
348
|
+
return Boolean(argv[1] && path.resolve(argv[1]) === __filename);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isCliEntrypoint()) {
|
|
352
|
+
try {
|
|
353
|
+
const report = runRalphLoop(parseArgs(process.argv.slice(2)));
|
|
354
|
+
process.stdout.write(`\n[ralph-loop] Report: ${report.artifacts.jsonPath}\n`);
|
|
355
|
+
if (report.steps.some((step) => step.status === 'failed')) {
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(`[ralph-loop] Fatal: ${err.message}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
DEFAULT_ARTIFACT_DIR,
|
|
366
|
+
RALPH_STATE_PATHS,
|
|
367
|
+
VALID_MODES,
|
|
368
|
+
buildRalphSteps,
|
|
369
|
+
isCliEntrypoint,
|
|
370
|
+
normalizeMode,
|
|
371
|
+
parseArgs,
|
|
372
|
+
renderMarkdownReport,
|
|
373
|
+
runRalphLoop,
|
|
374
|
+
runStep,
|
|
375
|
+
writeReports,
|
|
376
|
+
};
|