thumbgate 0.9.14 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +1 -0
  5. package/adapters/README.md +1 -1
  6. package/adapters/chatgpt/openapi.yaml +105 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/forge/forge.yaml +28 -0
  10. package/adapters/mcp/server-stdio.js +41 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +18 -3
  13. package/config/mcp-allowlists.json +11 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +7 -5
  16. package/plugins/amp-skill/INSTALL.md +3 -4
  17. package/plugins/amp-skill/SKILL.md +0 -1
  18. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  19. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  20. package/plugins/claude-skill/INSTALL.md +1 -2
  21. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  22. package/plugins/codex-profile/.mcp.json +1 -1
  23. package/plugins/codex-profile/INSTALL.md +1 -1
  24. package/plugins/codex-profile/README.md +1 -1
  25. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/blog.html +1 -0
  28. package/public/dashboard.html +1 -1
  29. package/public/guide.html +1 -1
  30. package/public/index.html +8 -4
  31. package/public/learn/agent-harness-pattern.html +1 -1
  32. package/public/learn/ai-agent-persistent-memory.html +1 -1
  33. package/public/learn/mcp-pre-action-gates-explained.html +1 -1
  34. package/public/learn/stop-ai-agent-force-push.html +1 -1
  35. package/public/learn/vibe-coding-safety-net.html +1 -1
  36. package/public/learn.html +1 -1
  37. package/public/lessons.html +1 -1
  38. package/public/pro.html +1 -1
  39. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/async-job-runner.js +84 -24
  42. package/scripts/auto-wire-hooks.js +59 -1
  43. package/scripts/context-manager.js +330 -0
  44. package/scripts/dashboard.js +1 -1
  45. package/scripts/distribution-surfaces.js +12 -0
  46. package/scripts/ensure-repo-bootstrap.js +15 -14
  47. package/scripts/export-hf-dataset.js +293 -0
  48. package/scripts/gates-engine.js +96 -10
  49. package/scripts/hook-auto-capture.sh +1 -1
  50. package/scripts/hosted-job-launcher.js +260 -0
  51. package/scripts/managed-dpo-export.js +91 -0
  52. package/scripts/obsidian-export.js +0 -1
  53. package/scripts/operational-integrity.js +50 -7
  54. package/scripts/prove-lancedb.js +62 -4
  55. package/scripts/publish-decision.js +16 -0
  56. package/scripts/self-healing-check.js +6 -1
  57. package/scripts/social-analytics/load-env.js +33 -2
  58. package/scripts/social-analytics/store.js +200 -2
  59. package/scripts/sync-version.js +18 -11
  60. package/scripts/tool-registry.js +48 -0
  61. package/scripts/train_from_feedback.py +0 -4
  62. package/scripts/workflow-sentinel.js +793 -0
  63. package/src/api/server.js +205 -27
  64. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
package/src/api/server.js CHANGED
@@ -52,6 +52,11 @@ const {
52
52
  const {
53
53
  buildCloudflareSandboxPlan,
54
54
  } = require('../../scripts/cloudflare-dynamic-sandbox');
55
+ const {
56
+ listJobStates,
57
+ readJobState,
58
+ requestJobControl,
59
+ } = require('../../scripts/async-job-runner');
55
60
  const {
56
61
  loadModel,
57
62
  getReliability,
@@ -136,6 +141,13 @@ const {
136
141
  const {
137
142
  resolveAnalyticsWindow,
138
143
  } = require('../../scripts/analytics-window');
144
+ const {
145
+ launchDpoExportJob,
146
+ launchHarnessJob,
147
+ pauseQueuedJob,
148
+ cancelQueuedJob,
149
+ resumeHostedJob,
150
+ } = require('../../scripts/hosted-job-launcher');
139
151
  const {
140
152
  appendWorkflowSprintLead,
141
153
  advanceWorkflowSprintLead,
@@ -165,6 +177,9 @@ const SESSION_COOKIE_NAME = 'thumbgate_session_id';
165
177
  const ACQUISITION_COOKIE_NAME = 'thumbgate_acquisition_id';
166
178
  const VISITOR_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 90;
167
179
  const BUILD_METADATA = resolveBuildMetadata();
180
+ const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
181
+ const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
182
+ const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
168
183
 
169
184
  // ---------------------------------------------------------------------------
170
185
  // Stripe event tracking helpers
@@ -1215,6 +1230,7 @@ nav .container { display: flex; justify-content: space-between; align-items: cen
1215
1230
  .actions-bar { flex-direction: column; }
1216
1231
  }
1217
1232
  </style>
1233
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
1218
1234
  </head>
1219
1235
  <body>
1220
1236
  <nav><div class="container">
@@ -1545,6 +1561,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
1545
1561
  font-size: 14px;
1546
1562
  }
1547
1563
  </style>
1564
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
1548
1565
  </head>
1549
1566
  <body>
1550
1567
  <main>
@@ -1824,6 +1841,7 @@ function renderCheckoutCancelledPage(runtimeConfig) {
1824
1841
  margin-top: 12px;
1825
1842
  }
1826
1843
  </style>
1844
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
1827
1845
  </head>
1828
1846
  <body>
1829
1847
  <main>
@@ -1996,6 +2014,7 @@ function renderWorkflowSprintIntakeResultPage(runtimeConfig, { title, detail, le
1996
2014
  border: 1px solid var(--line);
1997
2015
  }
1998
2016
  </style>
2017
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
1999
2018
  </head>
2000
2019
  <body>
2001
2020
  <main>
@@ -2170,6 +2189,56 @@ function resolveSafePath(inputPath, { mustExist = false, safeDataDir } = {}) {
2170
2189
  return resolved;
2171
2190
  }
2172
2191
 
2192
+ function resolveDpoExportPaths(body = {}, options = {}) {
2193
+ const { safeDataDir, fallbackMemoryLogPath = null } = options;
2194
+ return {
2195
+ inputPath: body.inputPath
2196
+ ? resolveSafePath(body.inputPath, { mustExist: true, safeDataDir })
2197
+ : null,
2198
+ memoryLogPath: body.memoryLogPath
2199
+ ? resolveSafePath(body.memoryLogPath, { mustExist: true, safeDataDir })
2200
+ : null,
2201
+ outputPath: body.outputPath
2202
+ ? resolveSafePath(body.outputPath, { safeDataDir })
2203
+ : null,
2204
+ fallbackMemoryLogPath,
2205
+ };
2206
+ }
2207
+
2208
+ function loadDpoExportMemories({ inputPath, memoryLogPath, fallbackMemoryLogPath = null }) {
2209
+ if (inputPath) {
2210
+ const raw = fs.readFileSync(inputPath, 'utf-8');
2211
+ const parsedMemories = JSON.parse(raw);
2212
+ return Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
2213
+ }
2214
+
2215
+ return readJSONL(memoryLogPath || fallbackMemoryLogPath || DEFAULT_LOCAL_MEMORY_LOG);
2216
+ }
2217
+
2218
+ function parseJobStatuses(value) {
2219
+ if (!value) return [];
2220
+ return String(value)
2221
+ .split(',')
2222
+ .map((entry) => entry.trim())
2223
+ .filter(Boolean);
2224
+ }
2225
+
2226
+ function readHostedJobOrThrow(jobId) {
2227
+ const state = readJobState(jobId);
2228
+ if (!state) {
2229
+ throw createHttpError(404, `Job not found: ${jobId}`);
2230
+ }
2231
+ return state;
2232
+ }
2233
+
2234
+ function normalizeJobIdFromPath(pathname, suffix = '') {
2235
+ const pattern = suffix
2236
+ ? new RegExp(`^/v1/jobs/([^/]+)${suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
2237
+ : /^\/v1\/jobs\/([^/]+)$/;
2238
+ const match = pathname.match(pattern);
2239
+ return match ? decodeURIComponent(match[1]) : null;
2240
+ }
2241
+
2173
2242
  function createApiServer() {
2174
2243
  const expectedApiKey = getExpectedApiKey();
2175
2244
 
@@ -2779,7 +2848,7 @@ async function addContext(){
2779
2848
  version: pkg.version,
2780
2849
  status: 'ok',
2781
2850
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
2782
- endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
2851
+ endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
2783
2852
  }, {}, {
2784
2853
  headOnly: isHeadRequest,
2785
2854
  });
@@ -3715,6 +3784,112 @@ async function addContext(){
3715
3784
  return;
3716
3785
  }
3717
3786
 
3787
+ if (req.method === 'POST' && pathname === '/v1/jobs/harness') {
3788
+ const body = await parseJsonBody(req);
3789
+ const identifier = body.harness || body.harnessId;
3790
+ if (!identifier) {
3791
+ throw createHttpError(400, 'harness is required');
3792
+ }
3793
+ const inputs = parseOptionalObject(body.inputs, 'inputs') || {};
3794
+ try {
3795
+ const launched = launchHarnessJob(identifier, inputs, {
3796
+ jobId: normalizeNullableText(body.jobId) || undefined,
3797
+ skill: normalizeNullableText(body.skill) || undefined,
3798
+ partnerProfile: normalizeNullableText(body.partnerProfile) || undefined,
3799
+ autoImprove: body.autoImprove !== false,
3800
+ });
3801
+ sendJson(res, 202, {
3802
+ accepted: true,
3803
+ jobId: launched.jobId,
3804
+ status: launched.state.status,
3805
+ launchMode: launched.launchMode,
3806
+ pid: launched.pid,
3807
+ statusUrl: `/v1/jobs/${encodeURIComponent(launched.jobId)}`,
3808
+ job: launched.state,
3809
+ });
3810
+ } catch (err) {
3811
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid hosted harness request');
3812
+ }
3813
+ return;
3814
+ }
3815
+
3816
+ if (req.method === 'GET' && pathname === '/v1/jobs') {
3817
+ const limit = Number(parsed.searchParams.get('limit') || 20);
3818
+ const statuses = parseJobStatuses(parsed.searchParams.get('status'));
3819
+ const jobs = listJobStates({
3820
+ limit: Number.isFinite(limit) ? limit : 20,
3821
+ statuses,
3822
+ });
3823
+ sendJson(res, 200, {
3824
+ total: jobs.length,
3825
+ jobs,
3826
+ });
3827
+ return;
3828
+ }
3829
+
3830
+ {
3831
+ const jobId = normalizeJobIdFromPath(pathname);
3832
+ if (req.method === 'GET' && jobId) {
3833
+ sendJson(res, 200, {
3834
+ job: readHostedJobOrThrow(jobId),
3835
+ });
3836
+ return;
3837
+ }
3838
+ }
3839
+
3840
+ {
3841
+ const jobId = normalizeJobIdFromPath(pathname, '/control');
3842
+ if (req.method === 'POST' && jobId) {
3843
+ const state = readHostedJobOrThrow(jobId);
3844
+ const body = await parseJsonBody(req);
3845
+ const action = normalizeNullableText(body.action);
3846
+ if (!action || !JOB_CONTROL_ACTIONS.has(action)) {
3847
+ throw createHttpError(400, 'action must be one of pause, cancel, or resume');
3848
+ }
3849
+
3850
+ if (TERMINAL_JOB_STATUSES.has(state.status)) {
3851
+ throw createHttpError(409, `Job ${jobId} is already ${state.status}`);
3852
+ }
3853
+
3854
+ if (action === 'resume') {
3855
+ const launched = resumeHostedJob(jobId);
3856
+ sendJson(res, 202, {
3857
+ accepted: true,
3858
+ action,
3859
+ jobId,
3860
+ launchMode: launched.launchMode,
3861
+ pid: launched.pid,
3862
+ job: launched.state,
3863
+ });
3864
+ return;
3865
+ }
3866
+
3867
+ if (IDLE_JOB_STATUSES.has(state.status)) {
3868
+ const job = action === 'pause'
3869
+ ? pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
3870
+ : cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
3871
+ sendJson(res, 202, {
3872
+ accepted: true,
3873
+ action,
3874
+ jobId,
3875
+ job,
3876
+ });
3877
+ return;
3878
+ }
3879
+
3880
+ const metadata = parseOptionalObject(body.metadata, 'metadata') || {};
3881
+ const control = requestJobControl(jobId, action, metadata);
3882
+ sendJson(res, 202, {
3883
+ accepted: true,
3884
+ action,
3885
+ jobId,
3886
+ control,
3887
+ job: readHostedJobOrThrow(jobId),
3888
+ });
3889
+ return;
3890
+ }
3891
+ }
3892
+
3718
3893
  if (req.method === 'POST' && pathname === '/v1/gates/constraint') {
3719
3894
  const body = await parseJsonBody(req);
3720
3895
  if (!body.key || body.value === undefined) {
@@ -3943,33 +4118,38 @@ async function addContext(){
3943
4118
 
3944
4119
  if (req.method === 'POST' && pathname === '/v1/dpo/export') {
3945
4120
  const body = await parseJsonBody(req);
3946
- let memories = [];
4121
+ const paths = resolveDpoExportPaths(body, {
4122
+ safeDataDir: requestSafeDataDir,
4123
+ fallbackMemoryLogPath: requestFeedbackPaths.MEMORY_LOG_PATH,
4124
+ });
4125
+ const wantsAsync = body.async === true || normalizeNullableText(body.mode) === 'async';
3947
4126
 
3948
- if (body.inputPath) {
3949
- const safeInputPath = resolveSafePath(body.inputPath, {
3950
- mustExist: true,
3951
- safeDataDir: requestSafeDataDir,
3952
- });
3953
- const raw = fs.readFileSync(safeInputPath, 'utf-8');
3954
- const parsedMemories = JSON.parse(raw);
3955
- memories = Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
3956
- } else {
3957
- const localPath = body.memoryLogPath
3958
- ? resolveSafePath(body.memoryLogPath, {
3959
- mustExist: true,
3960
- safeDataDir: requestSafeDataDir,
3961
- })
3962
- : requestFeedbackPaths.MEMORY_LOG_PATH;
3963
- memories = readJSONL(localPath);
4127
+ if (wantsAsync) {
4128
+ try {
4129
+ const launched = launchDpoExportJob(paths, {
4130
+ jobId: normalizeNullableText(body.jobId) || undefined,
4131
+ });
4132
+ sendJson(res, 202, {
4133
+ accepted: true,
4134
+ async: true,
4135
+ jobId: launched.jobId,
4136
+ status: launched.state.status,
4137
+ outputPath: paths.outputPath,
4138
+ statusUrl: `/v1/jobs/${encodeURIComponent(launched.jobId)}`,
4139
+ launchMode: launched.launchMode,
4140
+ job: launched.state,
4141
+ });
4142
+ } catch (err) {
4143
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid DPO export request');
4144
+ }
4145
+ return;
3964
4146
  }
3965
4147
 
4148
+ const memories = loadDpoExportMemories(paths);
3966
4149
  const result = exportDpoFromMemories(memories);
3967
- if (body.outputPath) {
3968
- const safeOutputPath = resolveSafePath(body.outputPath, {
3969
- safeDataDir: requestSafeDataDir,
3970
- });
3971
- fs.mkdirSync(path.dirname(safeOutputPath), { recursive: true });
3972
- fs.writeFileSync(safeOutputPath, result.jsonl);
4150
+ if (paths.outputPath) {
4151
+ fs.mkdirSync(path.dirname(paths.outputPath), { recursive: true });
4152
+ fs.writeFileSync(paths.outputPath, result.jsonl);
3973
4153
  }
3974
4154
 
3975
4155
  sendJson(res, 200, {
@@ -3978,9 +4158,7 @@ async function addContext(){
3978
4158
  learnings: result.learnings.length,
3979
4159
  unpairedErrors: result.unpairedErrors.length,
3980
4160
  unpairedLearnings: result.unpairedLearnings.length,
3981
- outputPath: body.outputPath
3982
- ? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
3983
- : null,
4161
+ outputPath: paths.outputPath,
3984
4162
  });
3985
4163
  return;
3986
4164
  }