thumbgate 1.6.0 → 1.8.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +90 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +39 -5
- package/config/mcp-allowlists.json +10 -1
- package/package.json +13 -7
- package/public/index.html +2 -2
- package/scripts/autonomous-workflow.js +377 -0
- package/scripts/billing.js +4 -2
- package/scripts/feedback-loop.js +22 -0
- package/scripts/gates-engine.js +308 -5
- package/scripts/mailer/resend-mailer.js +210 -40
- package/scripts/statusline-context.js +207 -0
- package/scripts/statusline.sh +31 -14
- package/scripts/tool-registry.js +39 -0
- package/CHANGELOG.md +0 -702
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { ensureDir } = require('./fs-utils');
|
|
8
|
+
const {
|
|
9
|
+
executeJob,
|
|
10
|
+
readJobState,
|
|
11
|
+
resumeJob,
|
|
12
|
+
} = require('./async-job-runner');
|
|
13
|
+
const {
|
|
14
|
+
createCheckpoint,
|
|
15
|
+
advanceCheckpoint,
|
|
16
|
+
loadCheckpoint,
|
|
17
|
+
saveCheckpoint,
|
|
18
|
+
} = require('./workflow-gate-checkpoint');
|
|
19
|
+
const { appendWorkflowRun } = require('./workflow-runs');
|
|
20
|
+
|
|
21
|
+
function normalizeText(value) {
|
|
22
|
+
if (value === undefined || value === null) return '';
|
|
23
|
+
return String(value).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(value, fallback = 'workflow') {
|
|
27
|
+
// Avoid any `-+` quantifier in an edge-anchored regex (Sonar javascript:S5852
|
|
28
|
+
// still flags even the anchored form). Strip edge dashes with a linear scan.
|
|
29
|
+
const collapsed = normalizeText(value).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
30
|
+
let start = 0;
|
|
31
|
+
let end = collapsed.length;
|
|
32
|
+
while (start < end && collapsed.charCodeAt(start) === 45) start += 1;
|
|
33
|
+
while (end > start && collapsed.charCodeAt(end - 1) === 45) end -= 1;
|
|
34
|
+
const normalized = collapsed.slice(start, end);
|
|
35
|
+
return normalized || fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getWorkflowPaths(workflowId, cwd = process.cwd()) {
|
|
39
|
+
const rootDir = path.join(cwd, '.thumbgate', 'autonomous-workflows', workflowId);
|
|
40
|
+
return {
|
|
41
|
+
rootDir,
|
|
42
|
+
checkpointPath: path.join(rootDir, 'checkpoint.json'),
|
|
43
|
+
reportJsonPath: path.join(rootDir, 'report.json'),
|
|
44
|
+
reportMdPath: path.join(rootDir, 'report.md'),
|
|
45
|
+
planPath: path.join(rootDir, 'plan.json'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePlan(input, workflowId) {
|
|
50
|
+
if (Array.isArray(input)) {
|
|
51
|
+
return {
|
|
52
|
+
workflowId,
|
|
53
|
+
summary: input.map((step) => normalizeText(step)).filter(Boolean).join(' | ') || 'Execution plan ready',
|
|
54
|
+
steps: input
|
|
55
|
+
.map((step, index) => ({
|
|
56
|
+
id: `step_${index + 1}`,
|
|
57
|
+
description: normalizeText(step),
|
|
58
|
+
}))
|
|
59
|
+
.filter((step) => step.description),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input && typeof input === 'object') {
|
|
64
|
+
const steps = Array.isArray(input.steps)
|
|
65
|
+
? input.steps
|
|
66
|
+
.map((step, index) => {
|
|
67
|
+
if (typeof step === 'string') {
|
|
68
|
+
return {
|
|
69
|
+
id: `step_${index + 1}`,
|
|
70
|
+
description: normalizeText(step),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (step && typeof step === 'object') {
|
|
75
|
+
return {
|
|
76
|
+
id: normalizeText(step.id) || `step_${index + 1}`,
|
|
77
|
+
description: normalizeText(step.description || step.summary || step.name),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
})
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
workflowId,
|
|
88
|
+
summary: normalizeText(input.summary) || steps.map((step) => step.description).join(' | ') || 'Execution plan ready',
|
|
89
|
+
steps,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const summary = normalizeText(input) || 'Execution plan ready';
|
|
94
|
+
return {
|
|
95
|
+
workflowId,
|
|
96
|
+
summary,
|
|
97
|
+
steps: summary ? [{ id: 'step_1', description: summary }] : [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildDefaultPlan(spec, workflowId) {
|
|
102
|
+
const executionSteps = Array.isArray(spec.stages)
|
|
103
|
+
? spec.stages.map((stage, index) => normalizeText(stage && (stage.name || stage.context || stage.command)) || `Stage ${index + 1}`)
|
|
104
|
+
: [];
|
|
105
|
+
|
|
106
|
+
return normalizePlan({
|
|
107
|
+
summary: normalizeText(spec.planSummary) || `Run ${executionSteps.length || 0} execution stage(s) and verify output`,
|
|
108
|
+
steps: [
|
|
109
|
+
{ id: 'intent', description: normalizeText(spec.intent) || 'Intent captured' },
|
|
110
|
+
{ id: 'plan', description: 'Execution plan generated' },
|
|
111
|
+
...executionSteps.map((description, index) => ({
|
|
112
|
+
id: `execute_${index + 1}`,
|
|
113
|
+
description,
|
|
114
|
+
})),
|
|
115
|
+
{ id: 'verify', description: 'Verification loop completed' },
|
|
116
|
+
{ id: 'report', description: 'Evidence-backed report recorded' },
|
|
117
|
+
],
|
|
118
|
+
workflowId,
|
|
119
|
+
}, workflowId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolvePlan(spec, workflowId) {
|
|
123
|
+
if (typeof spec.plan === 'function') {
|
|
124
|
+
return normalizePlan(spec.plan(spec), workflowId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (spec.plan) {
|
|
128
|
+
return normalizePlan(spec.plan, workflowId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return buildDefaultPlan(spec, workflowId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildExecutionJob(spec, workflowId, paths, plan) {
|
|
135
|
+
return {
|
|
136
|
+
id: spec.jobId || `${workflowId}-execution`,
|
|
137
|
+
tags: Array.isArray(spec.tags) ? spec.tags : [],
|
|
138
|
+
skill: spec.skill || 'autonomous-workflow',
|
|
139
|
+
partnerProfile: spec.partnerProfile || null,
|
|
140
|
+
verificationMode: spec.verificationMode === 'none' ? 'none' : 'standard',
|
|
141
|
+
autoImprove: spec.autoImprove !== false,
|
|
142
|
+
recordFeedback: spec.recordFeedback !== false,
|
|
143
|
+
stages: Array.isArray(spec.stages) ? spec.stages : [],
|
|
144
|
+
metadata: {
|
|
145
|
+
workflowId,
|
|
146
|
+
planSummary: plan.summary,
|
|
147
|
+
workflowRoot: paths.rootDir,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeWorkflowPlan(paths, plan) {
|
|
153
|
+
ensureDir(paths.rootDir);
|
|
154
|
+
fs.writeFileSync(paths.planPath, `${JSON.stringify(plan, null, 2)}\n`, 'utf8');
|
|
155
|
+
return paths.planPath;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectEvidenceArtifacts(paths, executionResult, extraArtifacts = []) {
|
|
159
|
+
return [
|
|
160
|
+
paths.checkpointPath,
|
|
161
|
+
paths.planPath,
|
|
162
|
+
paths.reportJsonPath,
|
|
163
|
+
paths.reportMdPath,
|
|
164
|
+
executionResult && executionResult.jobStatePath ? executionResult.jobStatePath : null,
|
|
165
|
+
...extraArtifacts,
|
|
166
|
+
].filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeWorkflowReport(paths, report) {
|
|
170
|
+
ensureDir(paths.rootDir);
|
|
171
|
+
fs.writeFileSync(paths.reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
172
|
+
|
|
173
|
+
const markdown = [
|
|
174
|
+
`# ${report.workflowName}`,
|
|
175
|
+
'',
|
|
176
|
+
`- Workflow ID: ${report.workflowId}`,
|
|
177
|
+
`- Status: ${report.status}`,
|
|
178
|
+
`- Intent: ${report.intent}`,
|
|
179
|
+
`- Verification accepted: ${report.verification ? String(report.verification.accepted) : 'skipped'}`,
|
|
180
|
+
`- Evidence artifacts: ${report.evidenceArtifacts.length}`,
|
|
181
|
+
'',
|
|
182
|
+
'## Plan',
|
|
183
|
+
'',
|
|
184
|
+
report.plan.summary,
|
|
185
|
+
'',
|
|
186
|
+
...report.plan.steps.map((step) => `- ${step.id}: ${step.description}`),
|
|
187
|
+
'',
|
|
188
|
+
'## Execution',
|
|
189
|
+
'',
|
|
190
|
+
...report.execution.stageHistory.map((stage) => `- ${stage.name} @ ${stage.completedAt}`),
|
|
191
|
+
'',
|
|
192
|
+
'## Evidence Artifacts',
|
|
193
|
+
'',
|
|
194
|
+
...report.evidenceArtifacts.map((artifact) => `- ${artifact}`),
|
|
195
|
+
].join('\n');
|
|
196
|
+
|
|
197
|
+
fs.writeFileSync(paths.reportMdPath, `${markdown}\n`, 'utf8');
|
|
198
|
+
return {
|
|
199
|
+
json: paths.reportJsonPath,
|
|
200
|
+
markdown: paths.reportMdPath,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function recordAutonomousWorkflowRun(spec, report, evidenceArtifacts, feedbackDir) {
|
|
205
|
+
const proofBacked = report.status === 'completed'
|
|
206
|
+
&& (!report.verification || report.verification.accepted)
|
|
207
|
+
&& evidenceArtifacts.length > 0;
|
|
208
|
+
|
|
209
|
+
return appendWorkflowRun({
|
|
210
|
+
workflowId: report.workflowId,
|
|
211
|
+
workflowName: report.workflowName,
|
|
212
|
+
owner: spec.owner || 'automation',
|
|
213
|
+
runtime: 'node',
|
|
214
|
+
status: report.status,
|
|
215
|
+
customerType: spec.customerType || 'internal_dogfood',
|
|
216
|
+
teamId: spec.teamId || null,
|
|
217
|
+
reviewed: proofBacked,
|
|
218
|
+
reviewedBy: proofBacked ? (spec.reviewedBy || 'automation') : null,
|
|
219
|
+
proofBacked,
|
|
220
|
+
proofArtifacts: evidenceArtifacts,
|
|
221
|
+
source: spec.source || 'autonomous-workflow',
|
|
222
|
+
metadata: {
|
|
223
|
+
intent: report.intent,
|
|
224
|
+
planSummary: report.plan.summary,
|
|
225
|
+
verificationAttempts: report.verification ? report.verification.attempts : 0,
|
|
226
|
+
executionJobId: report.execution.jobId,
|
|
227
|
+
},
|
|
228
|
+
}, feedbackDir);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runAutonomousWorkflow(spec = {}, options = {}) {
|
|
232
|
+
const cwd = options.cwd || process.cwd();
|
|
233
|
+
const workflowId = normalizeText(spec.workflowId) || slugify(spec.name || spec.intent, 'autonomous-workflow');
|
|
234
|
+
const workflowName = normalizeText(spec.name) || `Autonomous workflow ${workflowId}`;
|
|
235
|
+
const intent = normalizeText(spec.intent) || 'Intent not provided';
|
|
236
|
+
const paths = getWorkflowPaths(workflowId, cwd);
|
|
237
|
+
const plan = resolvePlan(spec, workflowId);
|
|
238
|
+
|
|
239
|
+
writeWorkflowPlan(paths, plan);
|
|
240
|
+
|
|
241
|
+
let checkpoint = createCheckpoint({
|
|
242
|
+
workflowId,
|
|
243
|
+
phase: 'intent',
|
|
244
|
+
status: 'running',
|
|
245
|
+
intent: { summary: intent },
|
|
246
|
+
plan,
|
|
247
|
+
evidence: [paths.planPath],
|
|
248
|
+
metadata: {
|
|
249
|
+
workflowName,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
saveCheckpoint(checkpoint, paths.checkpointPath);
|
|
253
|
+
|
|
254
|
+
checkpoint = advanceCheckpoint(checkpoint, {
|
|
255
|
+
phase: 'plan',
|
|
256
|
+
status: 'running',
|
|
257
|
+
plan,
|
|
258
|
+
evidence: [paths.planPath],
|
|
259
|
+
});
|
|
260
|
+
saveCheckpoint(checkpoint, paths.checkpointPath);
|
|
261
|
+
|
|
262
|
+
const job = buildExecutionJob(spec, workflowId, paths, plan);
|
|
263
|
+
const executionResult = options.resume === true
|
|
264
|
+
? resumeJob(job.id, job)
|
|
265
|
+
: executeJob(job);
|
|
266
|
+
const jobState = readJobState(job.id);
|
|
267
|
+
|
|
268
|
+
checkpoint = advanceCheckpoint(checkpoint, {
|
|
269
|
+
phase: 'verify',
|
|
270
|
+
status: executionResult.status,
|
|
271
|
+
evidence: jobState && jobState.verification ? [paths.checkpointPath] : [],
|
|
272
|
+
metadata: {
|
|
273
|
+
executionJobId: job.id,
|
|
274
|
+
executionStatus: executionResult.status,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
saveCheckpoint(checkpoint, paths.checkpointPath);
|
|
278
|
+
|
|
279
|
+
const report = {
|
|
280
|
+
workflowId,
|
|
281
|
+
workflowName,
|
|
282
|
+
status: executionResult.status,
|
|
283
|
+
intent,
|
|
284
|
+
plan,
|
|
285
|
+
execution: {
|
|
286
|
+
jobId: job.id,
|
|
287
|
+
status: executionResult.status,
|
|
288
|
+
stageHistory: Array.isArray(jobState && jobState.stageHistory) ? jobState.stageHistory : [],
|
|
289
|
+
checkpointCount: Array.isArray(jobState && jobState.checkpoints) ? jobState.checkpoints.length : 0,
|
|
290
|
+
currentContext: jobState && jobState.currentContext ? jobState.currentContext : '',
|
|
291
|
+
jobStatePath: jobState ? path.join(getFeedbackDir(options.feedbackDir), 'jobs', job.id, 'state.json') : null,
|
|
292
|
+
},
|
|
293
|
+
verification: executionResult.phases ? executionResult.phases.verification : null,
|
|
294
|
+
phases: executionResult.phases || null,
|
|
295
|
+
timestamp: new Date().toISOString(),
|
|
296
|
+
evidenceArtifacts: [],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const evidenceArtifacts = collectEvidenceArtifacts(paths, report.execution, spec.proofArtifacts);
|
|
300
|
+
report.evidenceArtifacts = evidenceArtifacts;
|
|
301
|
+
|
|
302
|
+
checkpoint = advanceCheckpoint(checkpoint, {
|
|
303
|
+
phase: 'report',
|
|
304
|
+
status: executionResult.status,
|
|
305
|
+
report: {
|
|
306
|
+
status: report.status,
|
|
307
|
+
generatedAt: report.timestamp,
|
|
308
|
+
},
|
|
309
|
+
evidence: evidenceArtifacts,
|
|
310
|
+
});
|
|
311
|
+
saveCheckpoint(checkpoint, paths.checkpointPath);
|
|
312
|
+
|
|
313
|
+
writeWorkflowReport(paths, report);
|
|
314
|
+
report.workflowRun = recordAutonomousWorkflowRun(spec, report, evidenceArtifacts, options.feedbackDir);
|
|
315
|
+
fs.writeFileSync(paths.reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
316
|
+
|
|
317
|
+
return report;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getFeedbackDir(feedbackDir) {
|
|
321
|
+
if (feedbackDir) return feedbackDir;
|
|
322
|
+
return process.env.THUMBGATE_FEEDBACK_DIR || path.join(process.cwd(), '.thumbgate');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resumeAutonomousWorkflow(spec = {}, options = {}) {
|
|
326
|
+
return runAutonomousWorkflow(spec, { ...options, resume: true });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readWorkflowReport(workflowId, options = {}) {
|
|
330
|
+
const paths = getWorkflowPaths(workflowId, options.cwd || process.cwd());
|
|
331
|
+
if (!fs.existsSync(paths.reportJsonPath)) return null;
|
|
332
|
+
return JSON.parse(fs.readFileSync(paths.reportJsonPath, 'utf8'));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isCliInvocation(argv = process.argv) {
|
|
336
|
+
const invokedPath = argv[1];
|
|
337
|
+
return invokedPath ? path.resolve(invokedPath) === __filename : false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
341
|
+
const args = {};
|
|
342
|
+
for (const arg of argv) {
|
|
343
|
+
if (!arg.startsWith('--')) continue;
|
|
344
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
345
|
+
args[key] = rest.length > 0 ? rest.join('=') : true;
|
|
346
|
+
}
|
|
347
|
+
return args;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isCliInvocation()) {
|
|
351
|
+
const args = parseArgs();
|
|
352
|
+
if (!args.file) {
|
|
353
|
+
console.error('Usage: node scripts/autonomous-workflow.js --file=workflow.json [--resume]');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const specPath = path.resolve(args.file);
|
|
358
|
+
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
359
|
+
const report = args.resume ? resumeAutonomousWorkflow(spec) : runAutonomousWorkflow(spec);
|
|
360
|
+
console.log(JSON.stringify(report, null, 2));
|
|
361
|
+
process.exit(report.status === 'completed' ? 0 : 1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
buildDefaultPlan,
|
|
366
|
+
collectEvidenceArtifacts,
|
|
367
|
+
getWorkflowPaths,
|
|
368
|
+
normalizePlan,
|
|
369
|
+
parseArgs,
|
|
370
|
+
readWorkflowReport,
|
|
371
|
+
recordAutonomousWorkflowRun,
|
|
372
|
+
resumeAutonomousWorkflow,
|
|
373
|
+
runAutonomousWorkflow,
|
|
374
|
+
slugify,
|
|
375
|
+
writeWorkflowPlan,
|
|
376
|
+
writeWorkflowReport,
|
|
377
|
+
};
|
package/scripts/billing.js
CHANGED
|
@@ -511,6 +511,7 @@ function buildTrialActivationEmail({ customerEmail, apiKey, sessionId, planId, a
|
|
|
511
511
|
const origin = resolvePublicAppOrigin(appOrigin);
|
|
512
512
|
const dashboardUrl = joinPublicUrl(origin, '/dashboard');
|
|
513
513
|
const docsUrl = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
|
|
514
|
+
const supportEmail = process.env.THUMBGATE_SUPPORT_EMAIL || CONFIG.TRIAL_EMAIL_REPLY_TO || 'igor.ganapolsky@gmail.com';
|
|
514
515
|
const command = `npx thumbgate pro --activate --key=${apiKey || ''}`;
|
|
515
516
|
const subject = 'Your 7-day ThumbGate Pro trial is live';
|
|
516
517
|
const preheader = 'Activate Pro in one command, open the dashboard, and start blocking repeated AI coding mistakes.';
|
|
@@ -519,6 +520,7 @@ function buildTrialActivationEmail({ customerEmail, apiKey, sessionId, planId, a
|
|
|
519
520
|
const exampleFeedback = 'thumbs down: the answer skipped exact files and tests; next time include paths, commands, and verification evidence.';
|
|
520
521
|
const safeDashboardUrl = escapeHtml(dashboardUrl);
|
|
521
522
|
const safeDocsUrl = escapeHtml(docsUrl);
|
|
523
|
+
const safeSupportEmail = escapeHtml(supportEmail);
|
|
522
524
|
const safeCommand = escapeHtml(command);
|
|
523
525
|
const safeApiKey = escapeHtml(apiKey || '');
|
|
524
526
|
return {
|
|
@@ -544,7 +546,7 @@ function buildTrialActivationEmail({ customerEmail, apiKey, sessionId, planId, a
|
|
|
544
546
|
apiKey,
|
|
545
547
|
'',
|
|
546
548
|
`Verification evidence: ${docsUrl}`,
|
|
547
|
-
|
|
549
|
+
`Keep this key private. Questions? Reply to this email or write ${supportEmail}.`,
|
|
548
550
|
sessionId ? `Stripe session: ${sessionId}` : null,
|
|
549
551
|
planId ? `Plan: ${planId}` : null,
|
|
550
552
|
].filter(Boolean).join('\n'),
|
|
@@ -591,7 +593,7 @@ function buildTrialActivationEmail({ customerEmail, apiKey, sessionId, planId, a
|
|
|
591
593
|
|
|
592
594
|
<p style="margin:0;font-size:13px;line-height:1.6;color:#526273;">
|
|
593
595
|
Proof trail: <a href="${safeDocsUrl}" style="color:#087a91;">verification evidence</a>.
|
|
594
|
-
Keep this key private. Questions? Reply here or write <a href="mailto
|
|
596
|
+
Keep this key private. Questions? Reply here or write <a href="mailto:${safeSupportEmail}" style="color:#087a91;">${safeSupportEmail}</a>.
|
|
595
597
|
</p>
|
|
596
598
|
${sessionId ? `<p style="margin:12px 0 0;font-size:12px;line-height:1.5;color:#7a8790;">Stripe session: ${escapeHtml(sessionId)}</p>` : ''}
|
|
597
599
|
</td>
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -1265,6 +1265,8 @@ function captureFeedback(params) {
|
|
|
1265
1265
|
feedbackSession = openSession(feedbackEvent.id, signal, inferredContext);
|
|
1266
1266
|
} catch (_err) { /* non-critical */ }
|
|
1267
1267
|
|
|
1268
|
+
const correctiveActionsReminder = buildCorrectiveActionsReminder(correctiveActions);
|
|
1269
|
+
|
|
1268
1270
|
// Build result immediately — all remaining side-effects are deferred
|
|
1269
1271
|
const result = {
|
|
1270
1272
|
accepted: true,
|
|
@@ -1274,6 +1276,10 @@ function captureFeedback(params) {
|
|
|
1274
1276
|
memoryRecord,
|
|
1275
1277
|
_captureMs,
|
|
1276
1278
|
...(correctiveActions.length > 0 && { correctiveActions }),
|
|
1279
|
+
...(correctiveActionsReminder && {
|
|
1280
|
+
systemReminder: correctiveActionsReminder,
|
|
1281
|
+
thumbgateSystemReminder: correctiveActionsReminder,
|
|
1282
|
+
}),
|
|
1277
1283
|
...(reflection && { reflection }),
|
|
1278
1284
|
...(feedbackSession && { feedbackSession }),
|
|
1279
1285
|
...(synthesisResult && { synthesis: synthesisResult }),
|
|
@@ -1911,9 +1917,25 @@ function compactMemories() {
|
|
|
1911
1917
|
};
|
|
1912
1918
|
}
|
|
1913
1919
|
|
|
1920
|
+
function buildCorrectiveActionsReminder(correctiveActions = []) {
|
|
1921
|
+
if (!Array.isArray(correctiveActions) || correctiveActions.length === 0) return null;
|
|
1922
|
+
const lines = correctiveActions
|
|
1923
|
+
.slice(0, 3)
|
|
1924
|
+
.map((action) => {
|
|
1925
|
+
const type = String(action.type || action.source || 'corrective_action').replace(/_/g, ' ');
|
|
1926
|
+
const text = String(action.text || action.action || action.description || '').trim();
|
|
1927
|
+
if (!text) return null;
|
|
1928
|
+
return ` - ${type}: ${text.slice(0, 240)}`;
|
|
1929
|
+
})
|
|
1930
|
+
.filter(Boolean);
|
|
1931
|
+
if (lines.length === 0) return null;
|
|
1932
|
+
return `[ThumbGate] Corrective actions from prior lessons - apply before the next tool call:\n${lines.join('\n')}`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1914
1935
|
module.exports = {
|
|
1915
1936
|
captureFeedback,
|
|
1916
1937
|
compactMemories,
|
|
1938
|
+
buildCorrectiveActionsReminder,
|
|
1917
1939
|
analyzeFeedback,
|
|
1918
1940
|
buildPreventionRules,
|
|
1919
1941
|
writePreventionRules,
|