thumbgate 1.3.0 → 1.4.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +32 -13
  2. package/.claude-plugin/plugin.json +15 -2
  3. package/.well-known/llms.txt +60 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +109 -20
  6. package/adapters/README.md +1 -1
  7. package/adapters/chatgpt/openapi.yaml +168 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +84 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +200 -13
  13. package/bin/postinstall.js +8 -2
  14. package/config/budget.json +18 -0
  15. package/config/gates/code-edit.json +61 -0
  16. package/config/gates/db-write.json +61 -0
  17. package/config/gates/default.json +154 -3
  18. package/config/gates/deploy.json +61 -0
  19. package/config/github-about.json +2 -1
  20. package/config/merge-quality-checks.json +23 -0
  21. package/openapi/openapi.yaml +168 -0
  22. package/package.json +42 -10
  23. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  25. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  26. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex-profile/.mcp.json +1 -1
  28. package/plugins/codex-profile/INSTALL.md +27 -4
  29. package/plugins/codex-profile/README.md +33 -9
  30. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  31. package/plugins/opencode-profile/INSTALL.md +1 -1
  32. package/public/blog.html +73 -0
  33. package/public/compare/mem0.html +189 -0
  34. package/public/compare/speclock.html +180 -0
  35. package/public/compare.html +10 -2
  36. package/public/guide.html +2 -2
  37. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  38. package/public/guides/codex-cli-guardrails.html +158 -0
  39. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/pre-action-gates.html +162 -0
  41. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  42. package/public/index.html +136 -50
  43. package/public/lessons.html +33 -24
  44. package/public/llm-context.md +140 -0
  45. package/public/pro.html +24 -22
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/access-anomaly-detector.js +1 -1
  48. package/scripts/adk-consolidator.js +1 -5
  49. package/scripts/agent-security-hardening.js +4 -6
  50. package/scripts/agentic-data-pipeline.js +1 -3
  51. package/scripts/async-job-runner.js +1 -5
  52. package/scripts/audit-trail.js +1 -5
  53. package/scripts/background-agent-governance.js +2 -10
  54. package/scripts/billing.js +2 -16
  55. package/scripts/budget-enforcer.js +173 -0
  56. package/scripts/build-codex-plugin.js +152 -0
  57. package/scripts/check-congruence.js +132 -14
  58. package/scripts/commercial-offer.js +5 -7
  59. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  60. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  61. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  62. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  63. package/scripts/context-engine.js +21 -6
  64. package/scripts/contextfs.js +1 -21
  65. package/scripts/dashboard.js +20 -0
  66. package/scripts/decision-journal.js +341 -0
  67. package/scripts/delegation-runtime.js +1 -5
  68. package/scripts/distribution-surfaces.js +26 -0
  69. package/scripts/document-intake.js +927 -0
  70. package/scripts/ephemeral-agent-store.js +1 -8
  71. package/scripts/evolution-state.js +1 -5
  72. package/scripts/experiment-tracker.js +1 -5
  73. package/scripts/export-databricks-bundle.js +1 -5
  74. package/scripts/export-hf-dataset.js +1 -5
  75. package/scripts/export-training.js +1 -5
  76. package/scripts/feedback-attribution.js +1 -16
  77. package/scripts/feedback-history-distiller.js +1 -16
  78. package/scripts/feedback-loop.js +1 -5
  79. package/scripts/feedback-root-consolidator.js +2 -21
  80. package/scripts/feedback-session.js +49 -0
  81. package/scripts/feedback-to-rules.js +188 -28
  82. package/scripts/filesystem-search.js +1 -9
  83. package/scripts/fs-utils.js +104 -0
  84. package/scripts/gates-engine.js +149 -4
  85. package/scripts/github-about.js +32 -8
  86. package/scripts/gtm-revenue-loop.js +1 -5
  87. package/scripts/harness-selector.js +148 -0
  88. package/scripts/hosted-job-launcher.js +1 -5
  89. package/scripts/hybrid-feedback-context.js +7 -33
  90. package/scripts/intervention-policy.js +58 -1
  91. package/scripts/lesson-db.js +3 -18
  92. package/scripts/lesson-inference.js +194 -16
  93. package/scripts/lesson-retrieval.js +60 -24
  94. package/scripts/llm-client.js +59 -0
  95. package/scripts/managed-lesson-agent.js +183 -0
  96. package/scripts/marketing-experiment.js +8 -22
  97. package/scripts/meta-agent-loop.js +624 -0
  98. package/scripts/metered-billing.js +1 -1
  99. package/scripts/money-watcher.js +1 -4
  100. package/scripts/obsidian-export.js +1 -5
  101. package/scripts/operational-integrity.js +15 -3
  102. package/scripts/org-dashboard.js +6 -1
  103. package/scripts/per-step-scoring.js +2 -4
  104. package/scripts/pr-manager.js +201 -19
  105. package/scripts/pro-features.js +3 -2
  106. package/scripts/prompt-dlp.js +3 -3
  107. package/scripts/prove-adapters.js +1 -5
  108. package/scripts/prove-attribution.js +1 -5
  109. package/scripts/prove-automation.js +1 -3
  110. package/scripts/prove-cloudflare-sandbox.js +1 -3
  111. package/scripts/prove-data-pipeline.js +1 -3
  112. package/scripts/prove-intelligence.js +1 -3
  113. package/scripts/prove-lancedb.js +1 -5
  114. package/scripts/prove-local-intelligence.js +1 -3
  115. package/scripts/prove-packaged-runtime.js +75 -9
  116. package/scripts/prove-predictive-insights.js +1 -3
  117. package/scripts/prove-training-export.js +1 -3
  118. package/scripts/prove-workflow-contract.js +1 -5
  119. package/scripts/rate-limiter.js +3 -1
  120. package/scripts/reddit-dm-outreach.js +14 -4
  121. package/scripts/schedule-manager.js +3 -5
  122. package/scripts/security-scanner.js +448 -0
  123. package/scripts/self-distill-agent.js +579 -0
  124. package/scripts/semantic-dedup.js +115 -0
  125. package/scripts/skill-exporter.js +1 -3
  126. package/scripts/skill-generator.js +1 -5
  127. package/scripts/social-analytics/engagement-audit.js +1 -18
  128. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  129. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  130. package/scripts/social-analytics/publishers/zernio.js +51 -0
  131. package/scripts/social-pipeline.js +1 -3
  132. package/scripts/social-post-hourly.js +47 -4
  133. package/scripts/statusline-links.js +6 -5
  134. package/scripts/statusline.sh +29 -153
  135. package/scripts/sync-branch-protection.js +340 -0
  136. package/scripts/tessl-export.js +1 -3
  137. package/scripts/thumbgate-search.js +32 -1
  138. package/scripts/tool-kpi-tracker.js +1 -1
  139. package/scripts/tool-registry.js +106 -2
  140. package/scripts/vector-store.js +1 -5
  141. package/scripts/weekly-auto-post.js +1 -1
  142. package/scripts/workflow-sentinel.js +91 -0
  143. package/skills/thumbgate/SKILL.md +1 -1
  144. package/src/api/server.js +273 -4
  145. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  146. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -572,13 +572,24 @@ function runGh(args) {
572
572
  });
573
573
  }
574
574
 
575
- function findOpenPrForBranch({ branchName, runner = runGh } = {}) {
575
+ function resolveGitHubRepository(env = process.env) {
576
+ const repository = String(env.GITHUB_REPOSITORY || '').trim();
577
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) ? repository : null;
578
+ }
579
+
580
+ function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } = {}) {
576
581
  const normalizedBranch = String(branchName || '').trim();
577
582
  if (!normalizedBranch) return null;
578
- if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) {
583
+ if (!env.GH_TOKEN && !env.GITHUB_TOKEN) {
579
584
  return null;
580
585
  }
581
- const result = runner(['pr', 'list', '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url']);
586
+ const args = ['pr', 'list'];
587
+ const repository = resolveGitHubRepository(env);
588
+ if (repository) {
589
+ args.push('--repo', repository);
590
+ }
591
+ args.push('--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url');
592
+ const result = runner(args);
582
593
  if (!result || result.status !== 0) {
583
594
  return null;
584
595
  }
@@ -846,6 +857,7 @@ module.exports = {
846
857
  resolveGitBinary,
847
858
  resolveBaseRef,
848
859
  resolveCiBranchName,
860
+ resolveGitHubRepository,
849
861
  resolveRepoRoot,
850
862
  runCli,
851
863
  sanitizeGlobList,
@@ -19,6 +19,11 @@ const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
20
  const { readAuditLog, auditStats, skillAdherence } = require('./audit-trail');
21
21
  const { isProTier } = require('./rate-limiter');
22
+ const {
23
+ PRO_MONTHLY_PAYMENT_LINK,
24
+ PRO_PRICE_LABEL,
25
+ TEAM_PRICE_LABEL,
26
+ } = require('./commercial-offer');
22
27
 
23
28
  // ---------------------------------------------------------------------------
24
29
  // Agent Registry
@@ -181,7 +186,7 @@ function generateOrgDashboard(opts = {}) {
181
186
  };
182
187
 
183
188
  if (!pro) {
184
- summary.upgradeMessage = 'Upgrade to Pro for full org visibility all agents, all gates, all history. https://thumbgate-production.up.railway.app/checkout/pro';
189
+ summary.upgradeMessage = `Pro checkout: ${PRO_PRICE_LABEL}${PRO_MONTHLY_PAYMENT_LINK} | Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
185
190
  }
186
191
 
187
192
  return summary;
@@ -15,11 +15,9 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const { resolveFeedbackDir } = require('./feedback-paths');
18
+ const { ensureParentDir, readJsonl } = require('./fs-utils');
18
19
 
19
20
  function getFeedbackDir() { return resolveFeedbackDir(); }
20
- function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
21
- function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
22
-
23
21
  const SCORES_FILE = 'step-scores.jsonl';
24
22
  function getScoresPath() { return path.join(getFeedbackDir(), SCORES_FILE); }
25
23
 
@@ -62,7 +60,7 @@ function scoreStep(auditEntry) {
62
60
  function scoreAuditTrail(auditEntries) {
63
61
  const scores = auditEntries.map(scoreStep);
64
62
  const scoresPath = getScoresPath();
65
- ensureDir(scoresPath);
63
+ ensureParentDir(scoresPath);
66
64
  for (const s of scores) fs.appendFileSync(scoresPath, JSON.stringify(s) + '\n');
67
65
  return { scored: scores.length, scores };
68
66
  }
@@ -8,8 +8,19 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const { spawnSync } = require('child_process');
12
- const PR_FIELDS = 'number,state,mergeable,mergeStateStatus,statusCheckRollup,reviewDecision,isDraft,title';
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const { spawnSync } = require('node:child_process');
14
+ const PR_FIELDS = 'number,state,mergeable,mergeStateStatus,statusCheckRollup,reviewDecision,isDraft,title,url,headRefOid,baseRefName,mergeCommit,mergedAt,mergedBy';
15
+ const PR_CHECK_FIELDS = 'bucket,name,state,workflow,link,event';
16
+ const MERGE_QUALITY_CHECKS = JSON.parse(
17
+ fs.readFileSync(path.join(__dirname, '..', 'config', 'merge-quality-checks.json'), 'utf8')
18
+ );
19
+ const FIXED_GH_BINARIES = [
20
+ '/usr/bin/gh',
21
+ '/usr/local/bin/gh',
22
+ '/opt/homebrew/bin/gh',
23
+ ];
13
24
  const SUCCESSFUL_CHECK_CONCLUSIONS = new Set(['SUCCESS', 'SKIPPED', 'NEUTRAL']);
14
25
  const FAILING_CHECK_CONCLUSIONS = new Set([
15
26
  'ACTION_REQUIRED',
@@ -19,9 +30,71 @@ const FAILING_CHECK_CONCLUSIONS = new Set([
19
30
  'STARTUP_FAILURE',
20
31
  'TIMED_OUT',
21
32
  ]);
33
+ const PASSING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.passingBuckets || []).map((value) => String(value || '').toLowerCase()));
34
+ const PENDING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.pendingBuckets || []).map((value) => String(value || '').toLowerCase()));
35
+ const FAILING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.failingBuckets || []).map((value) => String(value || '').toLowerCase()));
22
36
 
23
- function runGh(args) {
24
- return spawnSync('gh', args, { encoding: 'utf-8' });
37
+ function assertSafeGhArgs(args) {
38
+ if (!Array.isArray(args) || args.length === 0) {
39
+ throw new Error('GH CLI args must be a non-empty array.');
40
+ }
41
+
42
+ return args.map((arg) => {
43
+ const normalized = String(arg ?? '');
44
+ if (!normalized || /\0/.test(normalized)) {
45
+ throw new Error(`Unsafe GH CLI arg: ${arg}`);
46
+ }
47
+ return normalized;
48
+ });
49
+ }
50
+
51
+ function normalizePrNumber(prNumber, { allowEmpty = true } = {}) {
52
+ const normalized = String(prNumber ?? '').trim();
53
+ if (!normalized) {
54
+ if (allowEmpty) {
55
+ return '';
56
+ }
57
+ throw new Error('PR number is required.');
58
+ }
59
+
60
+ if (!/^[1-9]\d*$/.test(normalized)) {
61
+ throw new Error(`Unsafe PR number: ${prNumber}`);
62
+ }
63
+
64
+ return normalized;
65
+ }
66
+
67
+ function resolveGhBinary(options = {}) {
68
+ const accessSync = options.accessSync || fs.accessSync;
69
+ const candidates = [];
70
+ const configuredBinary = String(process.env.THUMBGATE_GH_BIN || '').trim();
71
+
72
+ if (configuredBinary) {
73
+ if (!path.isAbsolute(configuredBinary)) {
74
+ throw new Error(`Unsafe GH binary path: ${configuredBinary}`);
75
+ }
76
+ candidates.push(configuredBinary);
77
+ }
78
+
79
+ candidates.push(...FIXED_GH_BINARIES);
80
+
81
+ for (const candidate of candidates) {
82
+ try {
83
+ accessSync(candidate, fs.constants.X_OK);
84
+ return candidate;
85
+ } catch {
86
+ continue;
87
+ }
88
+ }
89
+
90
+ throw new Error(`Unable to locate GH CLI in fixed paths: ${candidates.join(', ')}`);
91
+ }
92
+
93
+ function runGh(args, options = {}) {
94
+ return spawnSync(resolveGhBinary(options), assertSafeGhArgs(args), {
95
+ encoding: 'utf-8',
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ });
25
98
  }
26
99
 
27
100
  function formatGhError(result) {
@@ -40,13 +113,14 @@ function isMissingCurrentBranchPr(result, prNumber) {
40
113
  * Fetch granular PR status using GH CLI
41
114
  */
42
115
  function getPrStatus(prNumber = '', runner = runGh) {
116
+ const normalizedPrNumber = normalizePrNumber(prNumber);
43
117
  const args = ['pr', 'view'];
44
- if (prNumber) args.push(prNumber);
118
+ if (normalizedPrNumber) args.push(normalizedPrNumber);
45
119
  args.push('--json', PR_FIELDS);
46
120
 
47
121
  const result = runner(args);
48
122
  if (result.status !== 0) {
49
- if (isMissingCurrentBranchPr(result, prNumber)) {
123
+ if (isMissingCurrentBranchPr(result, normalizedPrNumber)) {
50
124
  return null;
51
125
  }
52
126
 
@@ -55,6 +129,16 @@ function getPrStatus(prNumber = '', runner = runGh) {
55
129
  return JSON.parse(result.stdout);
56
130
  }
57
131
 
132
+ function getPrChecks(prNumber = '', runner = runGh) {
133
+ const normalizedPrNumber = normalizePrNumber(prNumber, { allowEmpty: false });
134
+ const result = runner(['pr', 'checks', normalizedPrNumber, '--json', PR_CHECK_FIELDS]);
135
+ if (result.status !== 0) {
136
+ throw new Error(`Failed to fetch PR checks: ${formatGhError(result)}`);
137
+ }
138
+
139
+ return JSON.parse(result.stdout || '[]');
140
+ }
141
+
58
142
  function listOpenPrs(runner = runGh) {
59
143
  const result = runner(['pr', 'list', '--state', 'open', '--json', PR_FIELDS]);
60
144
  if (result.status !== 0) {
@@ -87,6 +171,23 @@ function summarizeChecks(checks = []) {
87
171
 
88
172
  for (const check of checks) {
89
173
  const name = check.name || 'unknown-check';
174
+ const bucket = String(check.bucket || '').toLowerCase();
175
+ if (bucket) {
176
+ if (FAILING_BUCKETS.has(bucket)) {
177
+ failing.push(name);
178
+ continue;
179
+ }
180
+
181
+ if (PENDING_BUCKETS.has(bucket)) {
182
+ pending.push(name);
183
+ continue;
184
+ }
185
+
186
+ if (PASSING_BUCKETS.has(bucket)) {
187
+ continue;
188
+ }
189
+ }
190
+
90
191
  const conclusion = check.conclusion || null;
91
192
  const status = check.status || (conclusion ? 'COMPLETED' : 'UNKNOWN');
92
193
 
@@ -108,6 +209,11 @@ function summarizeChecks(checks = []) {
108
209
  return { failing, pending };
109
210
  }
110
211
 
212
+ function sleep(ms) {
213
+ if (!ms || ms <= 0) return;
214
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
215
+ }
216
+
111
217
  /**
112
218
  * Diagnose and resolve blockers autonomously
113
219
  */
@@ -140,17 +246,29 @@ async function resolveBlockers(pr, runner = runGh) {
140
246
  }
141
247
 
142
248
  // 3. Handle CI Failures
143
- const checkSummary = summarizeChecks(pr.statusCheckRollup || []);
249
+ let checks = pr.statusCheckRollup || [];
250
+ let checkSource = 'statusCheckRollup';
251
+
252
+ if (pr.number) {
253
+ try {
254
+ checks = getPrChecks(pr.number, runner);
255
+ checkSource = 'gh pr checks';
256
+ } catch (error) {
257
+ console.warn(`[PR Manager] Falling back to statusCheckRollup for PR #${pr.number}: ${error.message}`);
258
+ }
259
+ }
260
+
261
+ const checkSummary = summarizeChecks(checks);
144
262
  const failingChecks = checkSummary.failing;
145
263
 
146
264
  if (failingChecks.length > 0) {
147
- console.log(`[PR Manager] BLOCKED: ${failingChecks.length} failing CI checks.`);
148
- return { status: 'blocked', reason: 'ci_failure', checks: failingChecks };
265
+ console.log(`[PR Manager] BLOCKED: ${failingChecks.length} failing quality checks via ${checkSource}.`);
266
+ return { status: 'blocked', reason: 'ci_failure', checks: failingChecks, checkSource };
149
267
  }
150
268
 
151
269
  if (checkSummary.pending.length > 0) {
152
- console.log(`[PR Manager] BLOCKED: ${checkSummary.pending.length} CI checks still pending.`);
153
- return { status: 'blocked', reason: 'ci_pending', checks: checkSummary.pending };
270
+ console.log(`[PR Manager] BLOCKED: ${checkSummary.pending.length} quality checks still pending via ${checkSource}.`);
271
+ return { status: 'blocked', reason: 'ci_pending', checks: checkSummary.pending, checkSource };
154
272
  }
155
273
 
156
274
  // 4. Handle Review Blockers
@@ -173,25 +291,74 @@ async function resolveBlockers(pr, runner = runGh) {
173
291
  return { status: 'pending', reason: 'unknown_state' };
174
292
  }
175
293
 
294
+ function waitForMergeCommit(prNumber, runner = runGh, options = {}) {
295
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 300000;
296
+ const intervalMs = Number.isFinite(options.intervalMs) ? options.intervalMs : 10000;
297
+ const startedAt = Date.now();
298
+
299
+ do {
300
+ const pr = getPrStatus(prNumber, runner);
301
+ if (pr && String(pr.state || '').toUpperCase() === 'MERGED' && pr.mergeCommit && pr.mergeCommit.oid) {
302
+ return {
303
+ finalized: true,
304
+ merged: true,
305
+ mergeCommit: pr.mergeCommit.oid,
306
+ mergedAt: pr.mergedAt || null,
307
+ mergedBy: pr.mergedBy && pr.mergedBy.login ? pr.mergedBy.login : null,
308
+ pr,
309
+ };
310
+ }
311
+
312
+ if (pr && String(pr.state || '').toUpperCase() === 'CLOSED') {
313
+ return {
314
+ finalized: true,
315
+ merged: false,
316
+ reason: 'closed_without_merge',
317
+ pr,
318
+ };
319
+ }
320
+
321
+ if (intervalMs <= 0) {
322
+ break;
323
+ }
324
+
325
+ if ((Date.now() - startedAt + intervalMs) > timeoutMs) {
326
+ break;
327
+ }
328
+
329
+ sleep(intervalMs);
330
+ } while ((Date.now() - startedAt) <= timeoutMs);
331
+
332
+ return {
333
+ finalized: false,
334
+ merged: false,
335
+ reason: 'merge_commit_pending',
336
+ };
337
+ }
338
+
176
339
  /**
177
340
  * Perform autonomous merge
178
341
  */
179
- function performMerge(prNumber, runner = runGh) {
180
- const args = ['pr', 'merge', prNumber.toString(), '--squash', '--delete-branch', '--auto'];
181
- console.log(`[PR Manager] Initiating protected squash merge for PR #${prNumber}...`);
342
+ function performMerge(prNumber, runner = runGh, options = {}) {
343
+ const normalizedPrNumber = normalizePrNumber(prNumber, { allowEmpty: false });
344
+ const args = ['pr', 'merge', normalizedPrNumber, '--squash', '--delete-branch'];
345
+ console.log(`[PR Manager] Initiating protected squash merge for PR #${normalizedPrNumber}...`);
182
346
  const result = runner(args);
183
347
  if (result.status === 0) {
184
348
  const output = `${result.stdout || ''}\n${result.stderr || ''}`;
185
- const mode = /merge queue|queued|auto-merge/i.test(output) ? 'queued_or_auto' : 'merged';
186
- console.log(`[PR Manager] Merge accepted for PR #${prNumber} (${mode}).`);
187
- return { ok: true, mode, args };
349
+ const mode = /merge queue|queued/i.test(output) ? 'queued' : 'merged';
350
+ console.log(`[PR Manager] Merge accepted for PR #${normalizedPrNumber} (${mode}).`);
351
+ const mergeStatus = options.waitForMerge === false
352
+ ? { finalized: false, merged: false, reason: 'merge_commit_pending' }
353
+ : waitForMergeCommit(normalizedPrNumber, runner, options);
354
+ return { ok: true, mode, args, ...mergeStatus };
188
355
  } else {
189
356
  console.error(`[PR Manager] Merge failed: ${formatGhError(result)}`);
190
357
  return { ok: false, mode: 'failed', args, error: formatGhError(result) };
191
358
  }
192
359
  }
193
360
 
194
- async function managePrs(prNumber = '', runner = runGh) {
361
+ async function managePrs(prNumber = '', runner = runGh, options = {}) {
195
362
  const prs = loadManagedPrs(prNumber, runner).filter(Boolean);
196
363
 
197
364
  if (prs.length === 0) {
@@ -203,9 +370,18 @@ async function managePrs(prNumber = '', runner = runGh) {
203
370
  for (const pr of prs) {
204
371
  const outcome = await resolveBlockers(pr, runner);
205
372
  if (outcome.status === 'ready') {
206
- const mergeResult = performMerge(pr.number, runner);
373
+ const mergeResult = performMerge(pr.number, runner, options);
207
374
  outcome.mergeRequested = mergeResult.ok;
208
375
  outcome.mergeMode = mergeResult.mode;
376
+ if (mergeResult.mergeCommit) {
377
+ outcome.mergeCommit = mergeResult.mergeCommit;
378
+ }
379
+ if (mergeResult.finalized !== undefined) {
380
+ outcome.mergeFinalized = mergeResult.finalized;
381
+ }
382
+ if (mergeResult.reason) {
383
+ outcome.mergeResolution = mergeResult.reason;
384
+ }
209
385
  }
210
386
 
211
387
  results.push({
@@ -229,11 +405,17 @@ if (require.main === module) {
229
405
  }
230
406
 
231
407
  module.exports = {
408
+ assertSafeGhArgs,
232
409
  getPrStatus,
410
+ getPrChecks,
233
411
  listOpenPrs,
234
412
  isOpenPr,
235
413
  loadManagedPrs,
414
+ normalizePrNumber,
236
415
  resolveBlockers,
416
+ resolveGhBinary,
417
+ waitForMergeCommit,
237
418
  performMerge,
238
419
  managePrs,
420
+ summarizeChecks,
239
421
  };
@@ -3,6 +3,7 @@ const { isProLicensed } = require('./license');
3
3
  const {
4
4
  PRO_MONTHLY_PAYMENT_LINK,
5
5
  PRO_PRICE_LABEL,
6
+ TEAM_PRICE_LABEL,
6
7
  } = require('./commercial-offer');
7
8
 
8
9
  const PRO_URL = PRO_MONTHLY_PAYMENT_LINK;
@@ -30,8 +31,8 @@ function requirePro(
30
31
  const desc = descriptions[featureName] || featureName;
31
32
  write(
32
33
  `\n 🔒 Pro Feature Required: ${desc}\n` +
33
- ` Upgrade to ThumbGate Pro — ${PRO_PRICE_LABEL}:\n` +
34
- ` ${PRO_URL}\n` +
34
+ ` Pro: ${PRO_PRICE_LABEL} — ${PRO_URL}\n` +
35
+ ` Team: ${TEAM_PRICE_LABEL} after workflow qualification\n` +
35
36
  ` Or run: npx thumbgate pro\n\n`
36
37
  );
37
38
  return false;
@@ -16,6 +16,7 @@ const { SECRET_PATTERNS } = require('./secret-scanner');
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const { resolveFeedbackDir } = require('./feedback-paths');
19
+ const { ensureParentDir } = require('./fs-utils');
19
20
 
20
21
  const DLP_LOG_FILE = 'dlp-events.jsonl';
21
22
  const DEFAULT_MAX_SENSITIVITY = 'internal'; // block sensitive + restricted
@@ -24,7 +25,6 @@ function getDlpLogPath() {
24
25
  return path.join(resolveFeedbackDir(), DLP_LOG_FILE);
25
26
  }
26
27
 
27
- function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
28
28
 
29
29
  /**
30
30
  * Scan a tool call input for PII and secrets before execution.
@@ -75,7 +75,7 @@ function scanToolCallInput({ toolName, input, agentId, maxSensitivity } = {}) {
75
75
 
76
76
  // Log the event
77
77
  const logPath = getDlpLogPath();
78
- ensureDir(logPath);
78
+ ensureParentDir(logPath);
79
79
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
80
80
 
81
81
  return {
@@ -123,7 +123,7 @@ function detectShadowAction({ toolName, source, agentId } = {}) {
123
123
  gated: false,
124
124
  };
125
125
  const logPath = getShadowLogPath();
126
- ensureDir(logPath);
126
+ ensureParentDir(logPath);
127
127
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
128
128
  return { isShadow: true, event };
129
129
  }
@@ -8,15 +8,11 @@ const { startServer } = require('../src/api/server');
8
8
  const { handleRequest } = require('../adapters/mcp/server-stdio');
9
9
  const { validateSubagentProfiles, listSubagentProfiles } = require('./subagent-profiles');
10
10
  const { getAllowedTools } = require('./mcp-policy');
11
+ const { ensureDir } = require('./fs-utils');
11
12
 
12
13
  const ROOT = path.join(__dirname, '..');
13
14
  const DEFAULT_PROOF_DIR = path.join(ROOT, 'proof', 'compatibility');
14
15
 
15
- function ensureDir(dirPath) {
16
- if (!fs.existsSync(dirPath)) {
17
- fs.mkdirSync(dirPath, { recursive: true });
18
- }
19
- }
20
16
 
21
17
  function check(condition, message) {
22
18
  if (!condition) {
@@ -18,17 +18,13 @@ const path = require('path');
18
18
  const os = require('os');
19
19
  const { execSync } = require('child_process');
20
20
  const { escapeMarkdownTableCell } = require('./markdown-escape');
21
+ const { ensureDir } = require('./fs-utils');
21
22
 
22
23
  const ROOT = path.join(__dirname, '..');
23
24
 
24
25
  // Phase 5 node-runner test baseline (before Phase 6 attribution tests)
25
26
  const PHASE5_BASELINE = 142;
26
27
 
27
- function ensureDir(dirPath) {
28
- if (!fs.existsSync(dirPath)) {
29
- fs.mkdirSync(dirPath, { recursive: true });
30
- }
31
- }
32
28
 
33
29
  async function runProof(options = {}) {
34
30
  const proofDir = options.proofDir || process.env.THUMBGATE_PROOF_DIR || path.join(ROOT, 'proof');
@@ -22,13 +22,11 @@ const { traceForProofCheck, aggregateTraces } = require('./code-reasoning');
22
22
  const { runVerificationLoop } = require('./verification-loop');
23
23
  const { run: runGateCheck } = require('./gates-engine');
24
24
  const { evaluatePromptGuard } = require('./prompt-guard');
25
+ const { ensureDir } = require('./fs-utils');
25
26
 
26
27
  const ROOT = path.join(__dirname, '..');
27
28
  const DEFAULT_PROOF_DIR = path.join(ROOT, 'proof', 'automation');
28
29
 
29
- function ensureDir(dirPath) {
30
- if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
31
- }
32
30
 
33
31
  function check(condition, message) {
34
32
  if (!condition) throw new Error(message);
@@ -4,6 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { spawnSync } = require('child_process');
7
+ const { ensureDir } = require('./fs-utils');
7
8
 
8
9
  const {
9
10
  buildCloudflareSandboxPlan,
@@ -17,9 +18,6 @@ function npmCommand() {
17
18
  return process.platform === 'win32' ? 'npm.cmd' : 'npm';
18
19
  }
19
20
 
20
- function ensureDir(dirPath) {
21
- fs.mkdirSync(dirPath, { recursive: true });
22
- }
23
21
 
24
22
  function runCommand(command, args, cwd = ROOT) {
25
23
  const result = spawnSync(command, args, {
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
+ const { ensureDir } = require('./fs-utils');
6
7
 
7
8
  const ROOT = path.join(__dirname, '..');
8
9
 
@@ -400,9 +401,6 @@ async function run() {
400
401
  }
401
402
  }
402
403
 
403
- function ensureDir(dirPath) {
404
- fs.mkdirSync(dirPath, { recursive: true });
405
- }
406
404
 
407
405
  run().catch((error) => {
408
406
  console.error(error.message);
@@ -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
@@ -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 {