thumbgate 1.37.1 → 1.37.3

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 (47) hide show
  1. package/.agents/skills/article-syndication-pipeline/SKILL.md +37 -0
  2. package/.agents/skills/ci-gha-buildkite-patterns-not-clone/SKILL.md +72 -0
  3. package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
  4. package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
  5. package/.agents/skills/comet-session-logger/SKILL.md +59 -0
  6. package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
  7. package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
  8. package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
  9. package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  10. package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
  11. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +86 -0
  12. package/.claude-plugin/plugin.json +1 -1
  13. package/.well-known/mcp/server-card.json +1 -1
  14. package/adapters/claude/.mcp.json +2 -2
  15. package/adapters/forge/forge.yaml +3 -3
  16. package/adapters/future-agi/.mcp.json +1 -1
  17. package/adapters/future-agi/config.toml +1 -1
  18. package/adapters/future-agi/opencode.json +1 -1
  19. package/adapters/herdr/herdr-plugin.toml +2 -2
  20. package/adapters/mcp/server-stdio.js +1 -1
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/bin/cli.js +257 -0
  23. package/config/gate-templates.json +96 -0
  24. package/package.json +55 -12
  25. package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
  26. package/public/index.html +2 -2
  27. package/public/install.html +7 -7
  28. package/public/numbers.html +2 -2
  29. package/scripts/ci-gha-buildkite-patterns.js +393 -0
  30. package/scripts/cli-schema.js +90 -0
  31. package/scripts/cobble-hot-store-split.js +600 -0
  32. package/scripts/colab-compute-honesty.js +281 -0
  33. package/scripts/deeppattern-discipline-honesty.js +431 -0
  34. package/scripts/gates-engine.js +16 -5
  35. package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
  36. package/scripts/llm-obs-honesty.js +478 -0
  37. package/scripts/thumbgate-board-loop.js +432 -0
  38. package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
  39. package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
  40. package/scripts/token-shunt-honesty.js +502 -0
  41. package/scripts/typesafe-typed-questions.js +1138 -0
  42. package/server.json +2 -2
  43. package/skills/article-syndication-pipeline/SKILL.md +37 -0
  44. package/skills/comet-session-logger/SKILL.md +59 -0
  45. package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  46. package/src/index.js +10 -0
  47. package/src/integrations/ideabrowser-connector.js +190 -0
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * ThumbGate board loop — classify + act on the Issues + PR wall.
6
+ *
7
+ * Why this exists: GitHub's PR list "4/4" is not mergeable. Dependabot PRs
8
+ * go BEHIND after each Trunk land, and agent-automerge historically skipped
9
+ * `dependabot/*` plus left workflow_run events with no PR number.
10
+ *
11
+ * Never approve. Never --admin. Never gh pr merge --auto.
12
+ */
13
+
14
+ const { spawnSync } = require('node:child_process');
15
+ const path = require('node:path');
16
+ const fs = require('node:fs');
17
+
18
+ const FIXED_GH_BINARIES = Object.freeze([
19
+ '/opt/homebrew/bin/gh',
20
+ '/usr/local/bin/gh',
21
+ '/usr/bin/gh',
22
+ ]);
23
+
24
+ function resolveGhBinary(options = {}) {
25
+ const accessSync = options.accessSync || fs.accessSync;
26
+ const candidates = [];
27
+ const configuredBinary = options.ghBinary || process.env.THUMBGATE_GH_BINARY;
28
+ if (configuredBinary) {
29
+ if (!path.isAbsolute(configuredBinary)) {
30
+ throw new Error(`Unsafe GH binary path: ${configuredBinary}`);
31
+ }
32
+ candidates.push(configuredBinary);
33
+ }
34
+ candidates.push(...FIXED_GH_BINARIES);
35
+ for (const candidate of candidates) {
36
+ try {
37
+ accessSync(candidate, fs.constants.X_OK);
38
+ return candidate;
39
+ } catch {
40
+ continue;
41
+ }
42
+ }
43
+ throw new Error(`Unable to locate GH CLI in fixed paths: ${candidates.join(', ')}`);
44
+ }
45
+
46
+
47
+ const MARKER = '<!-- thumbgate-board-loop -->';
48
+ const SOURCE = 'thumbgate-board-loop';
49
+
50
+ const ECI_ISSUE_RE = /llm adjudicat|two-tier guardrail|adjudicator tier/i;
51
+ const UMBRELLA_ISSUE_RE = /gulli|gap map|agentic design patterns/i;
52
+ const IMPLEMENT_ISSUE_RE = /ideabrowser/i;
53
+
54
+ function normalizeBoolean(value) {
55
+ if (value === true || value === 1) return true;
56
+ if (value === false || value === 0 || value == null) return false;
57
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
58
+ }
59
+
60
+ function parseArgs(argv = process.argv.slice(2)) {
61
+ const out = {
62
+ json: false,
63
+ apply: false,
64
+ help: false,
65
+ maxUpdateBranch: 1,
66
+ maxPrManage: 1,
67
+ maxComments: 4,
68
+ skipPr: null,
69
+ };
70
+ for (const arg of argv) {
71
+ if (arg === '--json') out.json = true;
72
+ else if (arg === '--apply') out.apply = true;
73
+ else if (arg === '--help' || arg === '-h') out.help = true;
74
+ else if (arg.startsWith('--max-update-branch=')) {
75
+ out.maxUpdateBranch = Math.max(0, Number(arg.slice('--max-update-branch='.length)) || 0);
76
+ } else if (arg.startsWith('--max-pr-manage=')) {
77
+ out.maxPrManage = Math.max(0, Number(arg.slice('--max-pr-manage='.length)) || 0);
78
+ } else if (arg.startsWith('--max-comments=')) {
79
+ out.maxComments = Math.max(0, Number(arg.slice('--max-comments='.length)) || 0);
80
+ } else if (arg.startsWith('--skip-pr=')) {
81
+ out.skipPr = Number(arg.slice('--skip-pr='.length));
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function ciRollup(pr) {
88
+ const checks = Array.isArray(pr.statusCheckRollup) ? pr.statusCheckRollup : [];
89
+ if (pr.ciRollup) return String(pr.ciRollup).toUpperCase();
90
+ if (!checks.length && pr.ci) return String(pr.ci).toUpperCase();
91
+ let pending = 0;
92
+ let failing = 0;
93
+ for (const check of checks) {
94
+ const name = String(check.name || '');
95
+ if (/vercel|gitar|sonarcloud|merge blob/i.test(name)) continue;
96
+ const conclusion = String(check.conclusion || '').toUpperCase();
97
+ const status = String(check.status || '').toUpperCase();
98
+ if (status && status !== 'COMPLETED') {
99
+ pending += 1;
100
+ continue;
101
+ }
102
+ if (['FAILURE', 'TIMED_OUT', 'CANCELLED', 'STARTUP_FAILURE', 'ACTION_REQUIRED'].includes(conclusion)) {
103
+ failing += 1;
104
+ }
105
+ }
106
+ if (failing) return 'FAILURE';
107
+ if (pending) return 'PENDING';
108
+ return checks.length ? 'SUCCESS' : 'UNKNOWN';
109
+ }
110
+
111
+ function classifyPr(pr, options = {}) {
112
+ const number = Number(pr.number);
113
+ const mss = String(pr.mergeStateStatus || '').toUpperCase();
114
+ const mergeable = String(pr.mergeable || '').toUpperCase();
115
+ const head = String(pr.headRefName || '');
116
+ const ci = ciRollup(pr);
117
+ const queued = options.trunkQueuedNumbers instanceof Set
118
+ ? options.trunkQueuedNumbers
119
+ : new Set(options.trunkQueuedNumbers || []);
120
+
121
+ if (pr.isDraft && /^trunk-merge\/pr-/i.test(head)) {
122
+ return { number, class: 'trunk_queued', action: 'skip', ci, mss, reason: 'trunk draft already queued' };
123
+ }
124
+ if (pr.isDraft) {
125
+ return { number, class: 'draft', action: 'skip', ci, mss, reason: 'draft' };
126
+ }
127
+ if (queued.has(number) || queued.has(String(number))) {
128
+ return { number, class: 'trunk_queued', action: 'skip', ci, mss, reason: 'sibling trunk-merge PR open' };
129
+ }
130
+ if (mss === 'DIRTY' || mergeable === 'CONFLICTING') {
131
+ return { number, class: 'dirty', action: 'comment_needs_rebase', ci, mss, reason: 'conflicts vs main' };
132
+ }
133
+ if (ci === 'FAILURE') {
134
+ return { number, class: 'failing', action: 'triage', ci, mss, reason: 'required check red' };
135
+ }
136
+ if (ci === 'PENDING' || ci === 'UNKNOWN') {
137
+ return { number, class: 'pending', action: 'wait', ci, mss, reason: ci === 'UNKNOWN' ? 'no CI evidence yet' : 'required check pending' };
138
+ }
139
+ if (mss === 'BEHIND' && ci === 'SUCCESS') {
140
+ return { number, class: 'behind', action: 'update_branch', ci, mss, reason: 'green but behind tip' };
141
+ }
142
+ if ((mss === 'CLEAN' || mss === 'UNSTABLE' || mss === 'HAS_HOOKS') && ci === 'SUCCESS') {
143
+ return { number, class: 'ready', action: 'pr_manage', ci, mss, reason: 'terminal green, submit Trunk' };
144
+ }
145
+ if (mss === 'BLOCKED' && ci === 'SUCCESS') {
146
+ return {
147
+ number,
148
+ class: 'blocked_protection',
149
+ action: 'diagnose_threads',
150
+ ci,
151
+ mss,
152
+ reason: 'green but BLOCKED — conversation resolution or review, never approve',
153
+ };
154
+ }
155
+ return { number, class: 'unknown', action: 'report', ci, mss, reason: `unclassified mss=${mss} ci=${ci}` };
156
+ }
157
+
158
+ function classifyIssue(issue) {
159
+ const title = String(issue.title || '');
160
+ const number = Number(issue.number);
161
+ if (ECI_ISSUE_RE.test(title)) {
162
+ return { number, class: 'eci_pause', action: 'comment_keep', reason: 'net-new LLM adjudicator — ECI pause' };
163
+ }
164
+ if (UMBRELLA_ISSUE_RE.test(title)) {
165
+ return { number, class: 'umbrella', action: 'comment_keep', reason: 'design umbrella, not one-PR closable' };
166
+ }
167
+ if (IMPLEMENT_ISSUE_RE.test(title)) {
168
+ return { number, class: 'implement', action: 'comment_residual', reason: 'leftover AC not on origin/main' };
169
+ }
170
+ return { number, class: 'keep', action: 'comment_keep', reason: 'open product issue' };
171
+ }
172
+
173
+ function detectForbidden(text) {
174
+ const t = String(text || '');
175
+ const hits = [];
176
+ if (/gh pr merge[^\n]*--auto/.test(t)) hits.push('raw_auto_merge');
177
+ if (/event:\s*"APPROVE"|pulls\.createReview|gh pr review[^\n]*--approve/.test(t)) hits.push('approve');
178
+ if (/--admin\b/.test(t)) hits.push('admin_bypass');
179
+ return hits;
180
+ }
181
+
182
+ function trunkQueuedNumbers(prs) {
183
+ const set = new Set();
184
+ for (const pr of prs) {
185
+ const head = String(pr.headRefName || '');
186
+ const m = head.match(/^trunk-merge\/pr-(\d+)\//i);
187
+ if (m) set.add(Number(m[1]));
188
+ }
189
+ return set;
190
+ }
191
+
192
+ function alreadyMarked(comments) {
193
+ const list = Array.isArray(comments) ? comments : [];
194
+ return list.some((c) => String(c.body || c).includes(MARKER));
195
+ }
196
+
197
+ function issueCommentBody(classified, tipSha) {
198
+ const sha = tipSha || 'unverified';
199
+ if (classified.class === 'eci_pause') {
200
+ return `${MARKER}\n### Board-loop disposition\n\n**KEEP OPEN / ECI pause.** ${classified.reason}. Deterministic hard-block tier stays. Do not dual-build an LLM-in-the-gate SKU. Tip \`${sha}\`.`;
201
+ }
202
+ if (classified.class === 'implement') {
203
+ return `${MARKER}\n### Board-loop disposition\n\n**KEEP OPEN — IMPLEMENT residual.** ${classified.reason}. No close without a merge SHA on \`origin/main\`. Tip \`${sha}\`.`;
204
+ }
205
+ return `${MARKER}\n### Board-loop disposition\n\n**KEEP OPEN.** ${classified.reason}. Tip \`${sha}\`.`;
206
+ }
207
+
208
+ function dirtyPrCommentBody(classified) {
209
+ return `${MARKER}\nBoard-loop: **DIRTY/CONFLICTING** (ci=${classified.ci} mss=${classified.mss}). Needs successor rebase onto \`origin/main\`, not \`--admin\`. Not stale-closed (<30d failing PRs stay).`;
210
+ }
211
+
212
+ function runGh(args, runner) {
213
+ if (typeof runner === 'function') return runner(args);
214
+ return spawnSync(resolveGhBinary(), args, { encoding: 'utf8' });
215
+ }
216
+
217
+ function parseJson(stdout, fallback) {
218
+ try {
219
+ return JSON.parse(stdout || '');
220
+ } catch {
221
+ return fallback;
222
+ }
223
+ }
224
+
225
+ function loadBoard(runner) {
226
+ const prsRes = runGh([
227
+ 'pr', 'list', '--state', 'open', '--limit', '50',
228
+ '--json', 'number,title,author,isDraft,mergeable,mergeStateStatus,headRefName,url,statusCheckRollup,comments',
229
+ ], runner);
230
+ const issuesRes = runGh([
231
+ 'issue', 'list', '--state', 'open', '--limit', '50',
232
+ '--json', 'number,title,url,comments',
233
+ ], runner);
234
+ const prs = prsRes.status === 0 ? parseJson(prsRes.stdout, []) : [];
235
+ const issues = issuesRes.status === 0 ? parseJson(issuesRes.stdout, []) : [];
236
+ return { prs, issues, prsError: prsRes.status === 0 ? null : prsRes.stderr, issuesError: issuesRes.status === 0 ? null : issuesRes.stderr };
237
+ }
238
+
239
+ function planBoard(prs, issues) {
240
+ const queued = trunkQueuedNumbers(prs);
241
+ const prPlan = prs.map((pr) => {
242
+ const classified = classifyPr(pr, { trunkQueuedNumbers: queued });
243
+ return {
244
+ ...classified,
245
+ title: pr.title,
246
+ url: pr.url,
247
+ author: pr.author && (pr.author.login || pr.author),
248
+ headRefName: pr.headRefName,
249
+ alreadyMarked: alreadyMarked(pr.comments),
250
+ };
251
+ });
252
+ const issuePlan = issues.map((issue) => ({
253
+ ...classifyIssue(issue),
254
+ title: issue.title,
255
+ url: issue.url,
256
+ alreadyMarked: alreadyMarked(issue.comments),
257
+ }));
258
+ return { prPlan, issuePlan, queued: [...queued] };
259
+ }
260
+
261
+ function applyPlan(plan, options, runner) {
262
+ const actions = [];
263
+ const forbidden = detectForbidden(JSON.stringify(options));
264
+ if (forbidden.length) {
265
+ return { actions, refused: forbidden };
266
+ }
267
+
268
+ let updates = 0;
269
+ let manages = 0;
270
+ let comments = 0;
271
+ const skip = Number(options.skipPr) || null;
272
+
273
+ for (const row of plan.prPlan) {
274
+ if (skip && row.number === skip) continue;
275
+ if (row.action === 'update_branch' && updates < options.maxUpdateBranch) {
276
+ const res = runGh(['pr', 'update-branch', String(row.number)], runner);
277
+ actions.push({
278
+ kind: 'update_branch',
279
+ number: row.number,
280
+ ok: res.status === 0,
281
+ detail: (res.stderr || res.stdout || '').trim().slice(0, 200),
282
+ });
283
+ updates += 1;
284
+ } else if (row.action === 'pr_manage' && manages < options.maxPrManage) {
285
+ const script = path.join(__dirname, 'pr-manager.js');
286
+ const res = typeof runner === 'function'
287
+ ? runner(['node', script, String(row.number)])
288
+ : spawnSync(process.execPath, [script, String(row.number)], { encoding: 'utf8' });
289
+ actions.push({
290
+ kind: 'pr_manage',
291
+ number: row.number,
292
+ ok: res.status === 0,
293
+ detail: (res.stdout || res.stderr || '').trim().slice(0, 200),
294
+ });
295
+ manages += 1;
296
+ } else if (
297
+ row.action === 'comment_needs_rebase'
298
+ && !row.alreadyMarked
299
+ && comments < options.maxComments
300
+ ) {
301
+ const res = runGh([
302
+ 'pr', 'comment', String(row.number), '--body', dirtyPrCommentBody(row),
303
+ ], runner);
304
+ actions.push({
305
+ kind: 'pr_comment',
306
+ number: row.number,
307
+ ok: res.status === 0,
308
+ detail: 'needs-rebase',
309
+ });
310
+ comments += 1;
311
+ }
312
+ }
313
+
314
+ for (const row of plan.issuePlan) {
315
+ if (row.alreadyMarked) continue;
316
+ if (comments >= options.maxComments) break;
317
+ if (row.action === 'comment_keep' || row.action === 'comment_residual') {
318
+ const res = runGh([
319
+ 'issue', 'comment', String(row.number), '--body', issueCommentBody(row, options.tipSha),
320
+ ], runner);
321
+ actions.push({
322
+ kind: 'issue_comment',
323
+ number: row.number,
324
+ ok: res.status === 0,
325
+ class: row.class,
326
+ });
327
+ comments += 1;
328
+ }
329
+ }
330
+
331
+ return { actions, refused: [] };
332
+ }
333
+
334
+ function buildReport(options = {}, io = {}) {
335
+ const loaded = (io.prs || io.issues)
336
+ ? {
337
+ prs: io.prs || [],
338
+ issues: io.issues || [],
339
+ prsError: io.prsError || null,
340
+ issuesError: io.issuesError || null,
341
+ }
342
+ : loadBoard(io.runner);
343
+ const errors = [loaded.prsError, loaded.issuesError].filter(Boolean);
344
+ const plan = planBoard(loaded.prs || [], loaded.issues || []);
345
+ const counts = {};
346
+ for (const row of plan.prPlan) {
347
+ counts[row.class] = (counts[row.class] || 0) + 1;
348
+ }
349
+ let applied = { actions: [], refused: [] };
350
+ if (options.apply && errors.length === 0) {
351
+ applied = applyPlan(plan, options, io.runner);
352
+ }
353
+ return {
354
+ name: SOURCE,
355
+ ok: errors.length === 0,
356
+ errors,
357
+ never: ['approve a PR', 'gh pr merge --auto', '--admin', 'close DIRTY <30d without evidence'],
358
+ prs: plan.prPlan.length,
359
+ issues: plan.issuePlan.length,
360
+ trunkQueued: plan.queued,
361
+ counts,
362
+ prPlan: plan.prPlan,
363
+ issuePlan: plan.issuePlan,
364
+ applied: options.apply ? applied.actions : [],
365
+ refused: applied.refused,
366
+ };
367
+ }
368
+
369
+ function formatReport(report) {
370
+ const lines = [
371
+ 'ThumbGate board loop',
372
+ `PRs ${report.prs} Issues ${report.issues} Trunk-queued ${report.trunkQueued.join(',') || '(none)'}`,
373
+ `Counts ${JSON.stringify(report.counts)}`,
374
+ ];
375
+ for (const row of report.prPlan) {
376
+ lines.push(` PR #${row.number} ${row.class}/${row.action} ci=${row.ci} mss=${row.mss} ${row.title || ''}`);
377
+ }
378
+ for (const row of report.issuePlan) {
379
+ lines.push(` Issue #${row.number} ${row.class}/${row.action} ${row.title || ''}`);
380
+ }
381
+ if (report.applied.length) {
382
+ lines.push('Applied:');
383
+ for (const act of report.applied) {
384
+ lines.push(` ${act.kind} #${act.number} ok=${act.ok}`);
385
+ }
386
+ }
387
+ lines.push(`Never: ${(report.never || []).join('; ')}`);
388
+ return `${lines.join('\n')}\n`;
389
+ }
390
+
391
+ function printHelp() {
392
+ process.stdout.write(`Usage: node scripts/thumbgate-board-loop.js [--apply] [--json]
393
+
394
+ Classify every open ThumbGate PR + Issue. --apply update-branches at most one
395
+ BEHIND green PR, submits at most one READY PR via pr-manager, comments DIRTY
396
+ and Issues with a dedupe marker. Never approves. Never --admin.
397
+ `);
398
+ }
399
+
400
+ function main(argv = process.argv.slice(2)) {
401
+ const args = parseArgs(argv);
402
+ if (args.help) {
403
+ printHelp();
404
+ return 0;
405
+ }
406
+ const report = buildReport(args);
407
+ if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
408
+ else process.stdout.write(formatReport(report));
409
+ return report.ok ? 0 : 1;
410
+ }
411
+
412
+ module.exports = {
413
+ MARKER,
414
+ parseArgs,
415
+ ciRollup,
416
+ classifyPr,
417
+ classifyIssue,
418
+ detectForbidden,
419
+ trunkQueuedNumbers,
420
+ alreadyMarked,
421
+ planBoard,
422
+ applyPlan,
423
+ buildReport,
424
+ formatReport,
425
+ resolveGhBinary,
426
+ FIXED_GH_BINARIES,
427
+ main,
428
+ };
429
+
430
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
431
+ process.exitCode = main();
432
+ }