thumbgate 1.29.1 → 1.29.2

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 (35) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +61 -1
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/forge/forge.yaml +3 -3
  8. package/adapters/mcp/server-stdio.js +88 -2
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/commands/dashboard.md +11 -1
  11. package/commands/thumbgate-dashboard.md +23 -8
  12. package/config/agent-outcome-monitor-thresholds.json +63 -0
  13. package/config/evals/agent-outcomes-baseline.json +17 -0
  14. package/config/evals/agent-outcomes-golden.json +412 -0
  15. package/config/evals/prompt-eval-baseline.json +23 -0
  16. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  17. package/openapi/openapi.yaml +235 -0
  18. package/package.json +19 -6
  19. package/public/index.html +4 -2
  20. package/public/numbers.html +2 -2
  21. package/scripts/agent-outcome-eval.js +130 -0
  22. package/scripts/agent-outcome-monitor.js +261 -0
  23. package/scripts/agent-reasoning-traces.js +8 -9
  24. package/scripts/async-job-runner.js +107 -13
  25. package/scripts/durability/step.js +121 -12
  26. package/scripts/gates-engine.js +431 -18
  27. package/scripts/human-escalation.js +265 -0
  28. package/scripts/hybrid-feedback-context.js +93 -50
  29. package/scripts/judge-reward-function.js +30 -18
  30. package/scripts/prompt-eval.js +81 -4
  31. package/scripts/schedule-manager.js +249 -0
  32. package/scripts/task-outcomes.js +425 -0
  33. package/scripts/tool-contract-validator.js +287 -59
  34. package/scripts/tool-registry.js +143 -0
  35. package/src/api/server.js +127 -5
@@ -143,6 +143,8 @@ function handleRejectExpectation(checks, result, expected) {
143
143
 
144
144
  const wasRejected = result.accepted === false
145
145
  || result.status === 'rejected'
146
+ || result.status === 'clarification_required'
147
+ || result.needsClarification === true
146
148
  || result.actionType === 'no-action';
147
149
  checks.push({
148
150
  criterion: 'shouldReject',
@@ -466,8 +468,9 @@ function runSuiteObject(suite, options = {}) {
466
468
  const skipped = results.filter((r) => r.status === 'skip').length;
467
469
  const totalScore = results.length > 0
468
470
  ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length)
469
- : 100;
471
+ : 0;
470
472
  const minScore = options.minScore ?? 80;
473
+ const insufficientEvidence = results.length === 0;
471
474
 
472
475
  return {
473
476
  suite: suite.name,
@@ -478,8 +481,9 @@ function runSuiteObject(suite, options = {}) {
478
481
  skipped,
479
482
  score: totalScore,
480
483
  minScore,
481
- pass: totalScore >= minScore,
482
- noCases: results.length === 0,
484
+ pass: !insufficientEvidence && totalScore >= minScore,
485
+ noCases: insufficientEvidence,
486
+ evidenceStatus: insufficientEvidence ? 'insufficient_evidence' : 'measured',
483
487
  feedbackDerived: suite.source && suite.source.type === 'feedback-log',
484
488
  generatedAt: new Date().toISOString(),
485
489
  results,
@@ -685,6 +689,7 @@ function runSuite(suitePath = DEFAULT_SUITE, options = {}) {
685
689
 
686
690
  function compareReports(currentReport, baselineReport) {
687
691
  const baselineById = new Map((baselineReport?.results || []).map((result) => [result.id, result]));
692
+ const currentById = new Map((currentReport?.results || []).map((result) => [result.id, result]));
688
693
  const regressions = [];
689
694
  const improvements = [];
690
695
 
@@ -717,12 +722,83 @@ function compareReports(currentReport, baselineReport) {
717
722
  }
718
723
  }
719
724
 
725
+ for (const baseline of baselineReport?.results || []) {
726
+ if (currentById.has(baseline.id)) continue;
727
+ regressions.push({
728
+ id: baseline.id,
729
+ baselineScore: baseline.score,
730
+ currentScore: null,
731
+ delta: null,
732
+ baselineStatus: baseline.status,
733
+ currentStatus: 'missing',
734
+ });
735
+ }
736
+
720
737
  return {
721
738
  baselineSuite: baselineReport?.suite || null,
722
739
  baselineScore: Number.isFinite(Number(baselineReport?.score)) ? Number(baselineReport.score) : null,
723
740
  scoreDelta: Number.isFinite(Number(baselineReport?.score)) ? currentReport.score - Number(baselineReport.score) : null,
724
741
  regressions,
725
742
  improvements,
743
+ baselineCases: baselineById.size,
744
+ currentCases: currentById.size,
745
+ baselineCoverageRate: baselineById.size
746
+ ? Math.round((Array.from(baselineById.keys()).filter((id) => currentById.has(id)).length / baselineById.size) * 10000) / 10000
747
+ : null,
748
+ };
749
+ }
750
+
751
+ function logSafeResult(result, index) {
752
+ const allowedStatuses = new Set(['pass', 'fail', 'error', 'skip']);
753
+ return {
754
+ case: index + 1,
755
+ status: allowedStatuses.has(result.status) ? result.status : 'error',
756
+ score: Number(result.score || 0),
757
+ passCount: Number(result.passCount || 0),
758
+ totalChecks: Number(result.totalChecks || 0),
759
+ };
760
+ }
761
+
762
+ function logSafeReport(report, suite, fromFeedback) {
763
+ const source = suite?.source || {};
764
+ const comparison = report.comparison
765
+ ? {
766
+ baselineScore: Number(report.comparison.baselineScore || 0),
767
+ scoreDelta: Number(report.comparison.scoreDelta || 0),
768
+ regressionCount: Number(report.comparison.regressions?.length || 0),
769
+ improvementCount: Number(report.comparison.improvements?.length || 0),
770
+ baselineCases: Number(report.comparison.baselineCases || 0),
771
+ currentCases: Number(report.comparison.currentCases || 0),
772
+ baselineCoverageRate: report.comparison.baselineCoverageRate,
773
+ }
774
+ : undefined;
775
+ return {
776
+ suiteType: fromFeedback ? 'feedback-derived' : 'configured',
777
+ total: Number(report.total || 0),
778
+ passed: Number(report.passed || 0),
779
+ failed: Number(report.failed || 0),
780
+ errors: Number(report.errors || 0),
781
+ skipped: Number(report.skipped || 0),
782
+ score: Number(report.score || 0),
783
+ minScore: Number(report.minScore || 0),
784
+ pass: report.pass === true,
785
+ noCases: report.noCases === true,
786
+ evidenceStatus: report.evidenceStatus === 'measured' ? 'measured' : 'insufficient_evidence',
787
+ feedbackDerived: report.feedbackDerived === true,
788
+ syntheticCount: Number(report.syntheticCount || 0),
789
+ comparison,
790
+ results: (report.results || []).map(logSafeResult),
791
+ suiteDefinition: fromFeedback
792
+ ? {
793
+ version: Number(suite?.version || 0),
794
+ source: {
795
+ type: 'feedback-log',
796
+ totalEntries: Number(source.totalEntries || 0),
797
+ selectedCases: Number(source.selectedCases || 0),
798
+ },
799
+ evaluationCount: Number(suite?.evaluations?.length || 0),
800
+ }
801
+ : undefined,
726
802
  };
727
803
  }
728
804
 
@@ -847,7 +923,7 @@ if (isCliInvocation()) {
847
923
  }
848
924
 
849
925
  if (json) {
850
- console.log(JSON.stringify({ ...report, suiteDefinition: fromFeedback ? suite : undefined }, null, 2));
926
+ console.log(JSON.stringify(logSafeReport(report, suite, fromFeedback), null, 2));
851
927
  } else {
852
928
  console.log(`\n${report.suite}`);
853
929
  console.log('='.repeat(50));
@@ -883,6 +959,7 @@ module.exports = {
883
959
  gradeOutput,
884
960
  loadSuite,
885
961
  loadReport,
962
+ logSafeReport,
886
963
  compareReports,
887
964
  readJsonl,
888
965
  runEvaluation,
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { execSync } = require('child_process');
8
+ const { buildAgenticDataPipelineJobSpec } = require('./agentic-data-pipeline');
9
+ const { ensureDir } = require('./fs-utils');
10
+
11
+ const SCHEDULES_DIR = path.join(os.homedir(), '.thumbgate', 'schedules');
12
+ const PLIST_PREFIX = 'com.thumbgate.schedule';
13
+
14
+
15
+ function escapePlistString(value) {
16
+ return String(value || '')
17
+ .replace(/&/g, '&')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
22
+ }
23
+
24
+ /**
25
+ * Parse a simple cron-like spec into LaunchAgent calendar intervals
26
+ * Supports: "daily 9:00", "weekly monday 8:30", "hourly", "every 6h"
27
+ */
28
+ function parseCronSpec(spec) {
29
+ const s = spec.toLowerCase().trim();
30
+
31
+ if (s === 'hourly') {
32
+ return { Minute: 0 };
33
+ }
34
+
35
+ const everyHMatch = s.match(/^every\s+(\d+)\s*h/);
36
+ if (everyHMatch) {
37
+ return { Minute: 0 }; // LaunchAgent doesn't support "every Nh" natively, use hourly
38
+ }
39
+
40
+ const dailyMatch = s.match(/^daily\s+(\d{1,2}):(\d{2})$/);
41
+ if (dailyMatch) {
42
+ return { Hour: parseInt(dailyMatch[1]), Minute: parseInt(dailyMatch[2]) };
43
+ }
44
+
45
+ const weeklyMatch = s.match(/^weekly\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\s+(\d{1,2}):(\d{2})$/);
46
+ if (weeklyMatch) {
47
+ const dayMap = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6 };
48
+ return {
49
+ Weekday: dayMap[weeklyMatch[1]],
50
+ Hour: parseInt(weeklyMatch[2]),
51
+ Minute: parseInt(weeklyMatch[3]),
52
+ };
53
+ }
54
+
55
+ // Fallback: try to parse as "HH:MM" (daily)
56
+ const timeMatch = s.match(/^(\d{1,2}):(\d{2})$/);
57
+ if (timeMatch) {
58
+ return { Hour: parseInt(timeMatch[1]), Minute: parseInt(timeMatch[2]) };
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ function generatePlist(schedule) {
65
+ const label = escapePlistString(`${PLIST_PREFIX}.${schedule.id}`);
66
+ const interval = schedule.calendarInterval;
67
+
68
+ let intervalXml = '<dict>\n';
69
+ for (const [key, value] of Object.entries(interval)) {
70
+ intervalXml += ` <key>${key}</key>\n <integer>${value}</integer>\n`;
71
+ }
72
+ intervalXml += ' </dict>';
73
+
74
+ const logDir = escapePlistString(path.join(os.homedir(), '.thumbgate', 'logs'));
75
+ const workingDirectory = escapePlistString(schedule.workingDirectory || os.homedir());
76
+ const command = escapePlistString(schedule.command);
77
+ const homeDir = escapePlistString(os.homedir());
78
+ const escapedScheduleId = escapePlistString(schedule.id);
79
+
80
+ return `<?xml version="1.0" encoding="UTF-8"?>
81
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
82
+ <plist version="1.0">
83
+ <dict>
84
+ <key>Label</key>
85
+ <string>${label}</string>
86
+ <key>ProgramArguments</key>
87
+ <array>
88
+ <string>${process.execPath}</string>
89
+ <string>-e</string>
90
+ <string>${command}</string>
91
+ </array>
92
+ <key>WorkingDirectory</key>
93
+ <string>${workingDirectory}</string>
94
+ <key>StartCalendarInterval</key>
95
+ ${intervalXml}
96
+ <key>StandardOutPath</key>
97
+ <string>${logDir}/schedule-${escapedScheduleId}.log</string>
98
+ <key>StandardErrorPath</key>
99
+ <string>${logDir}/schedule-${escapedScheduleId}-error.log</string>
100
+ <key>EnvironmentVariables</key>
101
+ <dict>
102
+ <key>PATH</key>
103
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
104
+ <key>HOME</key>
105
+ <string>${homeDir}</string>
106
+ </dict>
107
+ </dict>
108
+ </plist>`;
109
+ }
110
+
111
+ function buildManagedScheduleCommand(params = {}) {
112
+ if (!params.jobFile) {
113
+ throw new Error('buildManagedScheduleCommand requires jobFile');
114
+ }
115
+
116
+ const runnerPath = path.join(__dirname, 'async-job-runner.js');
117
+ const jobFile = path.resolve(params.jobFile);
118
+ const autoResume = params.autoResume !== false;
119
+
120
+ return [
121
+ `const runner = require(${JSON.stringify(runnerPath)});`,
122
+ `const result = runner.runJobFromFile(${JSON.stringify(jobFile)}, ${JSON.stringify({ autoResume })});`,
123
+ 'process.stdout.write(JSON.stringify(result, null, 2) + "\\n");',
124
+ 'if (["failed", "cancelled"].includes(result.status)) process.exit(1);',
125
+ ].join(' ');
126
+ }
127
+
128
+ function buildAgenticDataPipelineSchedule(params = {}) {
129
+ const id = params.id || params.name || 'agentic-data-pipeline';
130
+ const jobFile = path.resolve(
131
+ params.jobFile || path.join(SCHEDULES_DIR, `${id}.job.json`)
132
+ );
133
+ const jobSpec = buildAgenticDataPipelineJobSpec({
134
+ jobId: id,
135
+ feedbackDir: params.feedbackDir,
136
+ outDir: params.outDir,
137
+ window: params.window,
138
+ liveBilling: params.liveBilling,
139
+ recordWorkflowRun: params.recordWorkflowRun,
140
+ });
141
+
142
+ return {
143
+ id,
144
+ jobFile,
145
+ jobSpec,
146
+ command: buildManagedScheduleCommand({
147
+ jobFile,
148
+ autoResume: params.autoResume !== false,
149
+ }),
150
+ };
151
+ }
152
+
153
+ function createSchedule(params) {
154
+ ensureDir(SCHEDULES_DIR);
155
+
156
+ const id = params.id || params.name || `sched_${Date.now()}`;
157
+ const calendarInterval = parseCronSpec(params.schedule);
158
+ if (!calendarInterval) {
159
+ return { success: false, error: `Cannot parse schedule: "${params.schedule}". Use formats like "daily 9:00", "weekly monday 8:30", "hourly"` };
160
+ }
161
+
162
+ const jobFile = params.jobFile ? path.resolve(params.jobFile) : null;
163
+ const command = params.command || (jobFile ? buildManagedScheduleCommand({
164
+ jobFile,
165
+ autoResume: params.autoResume !== false,
166
+ }) : null);
167
+
168
+ if (!command) {
169
+ return { success: false, error: 'Schedule requires command or jobFile' };
170
+ }
171
+
172
+ const schedule = {
173
+ id,
174
+ name: params.name || id,
175
+ description: params.description || '',
176
+ schedule: params.schedule,
177
+ command,
178
+ jobFile,
179
+ resumePolicy: jobFile ? (params.autoResume !== false ? 'auto_resume' : 'fresh_only') : null,
180
+ workingDirectory: params.workingDirectory || (jobFile ? path.dirname(jobFile) : process.cwd()),
181
+ calendarInterval,
182
+ createdAt: new Date().toISOString(),
183
+ };
184
+
185
+ // Save schedule metadata
186
+ const metaPath = path.join(SCHEDULES_DIR, `${id}.json`);
187
+ fs.writeFileSync(metaPath, JSON.stringify(schedule, null, 2), 'utf8');
188
+
189
+ // Generate and install LaunchAgent
190
+ if (process.platform === 'darwin') {
191
+ const plistContent = generatePlist(schedule);
192
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_PREFIX}.${id}.plist`);
193
+ const logDir = path.join(os.homedir(), '.thumbgate', 'logs');
194
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
195
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
196
+
197
+ fs.writeFileSync(plistPath, plistContent, 'utf8');
198
+ try {
199
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' });
200
+ } catch { /* not loaded */ }
201
+ try {
202
+ execSync(`launchctl load "${plistPath}"`, { stdio: 'pipe' });
203
+ } catch (e) {
204
+ return { success: false, error: `Failed to load LaunchAgent: ${e.message}`, schedule };
205
+ }
206
+
207
+ return { success: true, schedule, plistPath, message: `Schedule "${id}" created and loaded` };
208
+ }
209
+
210
+ // Linux keeps the schedule metadata so operators can install it via user crontab tooling.
211
+ return { success: true, schedule, message: `Schedule "${id}" saved for Linux crontab installation` };
212
+ }
213
+
214
+ function listSchedules() {
215
+ ensureDir(SCHEDULES_DIR);
216
+ const files = fs.readdirSync(SCHEDULES_DIR).filter(f => f.endsWith('.json'));
217
+ return files.map(f => {
218
+ try {
219
+ return JSON.parse(fs.readFileSync(path.join(SCHEDULES_DIR, f), 'utf8'));
220
+ } catch {
221
+ return { id: f.replace('.json', ''), error: 'corrupt' };
222
+ }
223
+ });
224
+ }
225
+
226
+ function deleteSchedule(id) {
227
+ const metaPath = path.join(SCHEDULES_DIR, `${id}.json`);
228
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_PREFIX}.${id}.plist`);
229
+
230
+ try {
231
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' });
232
+ } catch { /* not loaded */ }
233
+
234
+ if (fs.existsSync(plistPath)) fs.unlinkSync(plistPath);
235
+ if (fs.existsSync(metaPath)) fs.unlinkSync(metaPath);
236
+
237
+ return { success: true, message: `Schedule "${id}" deleted` };
238
+ }
239
+
240
+ module.exports = {
241
+ createSchedule,
242
+ listSchedules,
243
+ deleteSchedule,
244
+ escapePlistString,
245
+ generatePlist,
246
+ parseCronSpec,
247
+ buildManagedScheduleCommand,
248
+ buildAgenticDataPipelineSchedule,
249
+ };