thumbgate 1.27.18 → 1.27.19

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 (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. package/scripts/bot-detector.js +0 -50
@@ -70,6 +70,21 @@ const {
70
70
  verifyClaimEvidence,
71
71
  registerClaimGate,
72
72
  } = require('../../scripts/gates-engine');
73
+ const { mergeRepeatMetricIntoGateStats } = require('../../scripts/repeat-metric');
74
+ const {
75
+ detectNoop,
76
+ computeActionStateHash,
77
+ recordActionAttempt,
78
+ isRepeatAttempt,
79
+ } = require('../../scripts/noop-detect');
80
+ const { recordAuditEvent } = require('../../scripts/audit-trail');
81
+ const {
82
+ recordReceipt,
83
+ getReceiptForAction,
84
+ getRecentReceipts,
85
+ pairFeedbackWithReceipt,
86
+ buildReceiptContextEntries,
87
+ } = require('../../scripts/action-receipts');
73
88
  const {
74
89
  evaluateOperationalIntegrity,
75
90
  } = require('../../scripts/operational-integrity');
@@ -216,7 +231,7 @@ const {
216
231
  finalizeSession: finalizeFeedbackSession,
217
232
  } = require('../../scripts/feedback-session');
218
233
 
219
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.20.0' };
234
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.27.15' };
220
235
  const COMMERCE_CATEGORIES = [
221
236
  'product_recommendation',
222
237
  'brand_compliance',
@@ -368,6 +383,111 @@ function buildRecallResponse(args = {}) {
368
383
  return toTextResult(text);
369
384
  }
370
385
 
386
+ function buildSuggestFixResponse(args = {}) {
387
+ const context = String(args.context || '').trim();
388
+ const rawLimit = Number(args.limit);
389
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 5) : 3;
390
+
391
+ // If no context provided, return generic suggestion
392
+ if (!context) {
393
+ return toTextResult({
394
+ suggestions: [
395
+ {
396
+ action: 'Capture feedback about what went wrong so ThumbGate can learn and prevent recurrence.',
397
+ source: 'generic',
398
+ },
399
+ ],
400
+ query: '',
401
+ totalFound: 0,
402
+ });
403
+ }
404
+
405
+ // Search lessons via lesson-search module
406
+ const lessonModule = loadPrivateMcpModule('lessonSearch');
407
+ let lessonActions = [];
408
+ if (lessonModule) {
409
+ try {
410
+ const searchResult = lessonModule.searchLessons(context, { limit: 10 });
411
+ const results = Array.isArray(searchResult && searchResult.results) ? searchResult.results : [];
412
+ for (const result of results) {
413
+ const correctiveActions = (result.systemResponse && Array.isArray(result.systemResponse.correctiveActions))
414
+ ? result.systemResponse.correctiveActions
415
+ : [];
416
+ for (const action of correctiveActions) {
417
+ const text = String(action.text || '').trim();
418
+ if (text) {
419
+ lessonActions.push({
420
+ action: text,
421
+ source: action.source || `lesson:${result.id || 'unknown'}`,
422
+ score: result.score || 0,
423
+ });
424
+ }
425
+ }
426
+ // Also pick up lesson-level howToAvoid / actionNeeded when no explicit correctiveActions
427
+ if (correctiveActions.length === 0 && result.lesson) {
428
+ const text = result.lesson.howToAvoid || result.lesson.actionNeeded || '';
429
+ if (text) {
430
+ lessonActions.push({
431
+ action: String(text).trim(),
432
+ source: `lesson:${result.id || 'unknown'}`,
433
+ score: result.score || 0,
434
+ });
435
+ }
436
+ }
437
+ }
438
+ } catch {
439
+ // lesson search failure is non-fatal
440
+ }
441
+ }
442
+
443
+ // Search prevention rules directly via lesson-search module's helper
444
+ let ruleActions = [];
445
+ try {
446
+ const { readPreventionRuleMatches } = require('../../scripts/lesson-search');
447
+ const ruleMatches = readPreventionRuleMatches(context, limit);
448
+ for (const rule of ruleMatches) {
449
+ const text = rule.summary || rule.title || '';
450
+ if (text) {
451
+ ruleActions.push({
452
+ action: String(text).trim(),
453
+ source: `rule:${String(rule.title || 'prevention_rules').trim()}`,
454
+ score: rule.score || 0,
455
+ });
456
+ }
457
+ }
458
+ } catch {
459
+ // rule search failure is non-fatal
460
+ }
461
+
462
+ // Merge, deduplicate, sort by score, and take top `limit`
463
+ const seen = new Set();
464
+ const all = [...lessonActions, ...ruleActions]
465
+ .filter((item) => {
466
+ if (!item.action) return false;
467
+ const key = item.action.toLowerCase();
468
+ if (seen.has(key)) return false;
469
+ seen.add(key);
470
+ return true;
471
+ })
472
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
473
+ .slice(0, limit)
474
+ .map(({ action, source }) => ({ action, source }));
475
+
476
+ // If nothing matched, add generic fallback
477
+ if (all.length === 0) {
478
+ all.push({
479
+ action: 'No matching lessons or rules found. Capture feedback via capture_feedback so ThumbGate can learn from this failure.',
480
+ source: 'generic',
481
+ });
482
+ }
483
+
484
+ return toTextResult({
485
+ suggestions: all,
486
+ query: context,
487
+ totalFound: all.length,
488
+ });
489
+ }
490
+
371
491
  function buildDiagnoseFailureResponse(args = {}) {
372
492
  let intentPlan = null;
373
493
  const requestedProfile = args.mcpProfile || getActiveMcpProfile();
@@ -419,6 +539,28 @@ function buildContextPackResponse(args = {}) {
419
539
  maxChars: Number(args.maxChars || 6000),
420
540
  namespaces,
421
541
  });
542
+ // Feed outcome-paired action receipts into the pack so an action->outcome
543
+ // history is available alongside lessons/rules. Additive + guarded: a
544
+ // receipt-store failure must never break context pack construction.
545
+ try {
546
+ const receiptEntries = buildReceiptContextEntries(args.query || '', Number(args.maxItems || 8));
547
+ if (Array.isArray(receiptEntries) && receiptEntries.length && Array.isArray(pack.items)) {
548
+ for (const entry of receiptEntries) {
549
+ pack.items.push({
550
+ id: `action-receipt_${entry && entry.score != null ? entry.score : ''}_${pack.items.length}`,
551
+ namespace: 'action-receipts',
552
+ title: 'Action receipt outcome',
553
+ structuredContext: { rawContent: entry && entry.text ? String(entry.text) : '' },
554
+ tags: ['action-receipt', 'outcome-paired'],
555
+ score: entry && typeof entry.score === 'number' ? entry.score : 0,
556
+ });
557
+ }
558
+ if (!Array.isArray(pack.namespaces)) pack.namespaces = [];
559
+ if (!pack.namespaces.includes('action-receipts')) pack.namespaces.push('action-receipts');
560
+ }
561
+ } catch {
562
+ // ignore receipt enrichment failures
563
+ }
422
564
  return toTextResult(pack);
423
565
  }
424
566
 
@@ -532,7 +674,22 @@ function buildEstimateUncertaintyResponse(args = {}) {
532
674
 
533
675
  async function callTool(name, args = {}) {
534
676
  assertToolAllowed(name, getActiveMcpProfile());
535
- if (name !== 'workflow_sentinel') {
677
+
678
+ // Validate tool input contract against schema
679
+ const { TOOLS } = require('../../scripts/tool-registry');
680
+ const toolDef = TOOLS.find(t => t.name === name);
681
+ if (toolDef && toolDef.inputSchema) {
682
+ const { validateToolContract } = require('../../scripts/tool-contract-validator');
683
+ const validation = validateToolContract(toolDef.inputSchema, args);
684
+ if (!validation.valid) {
685
+ const err = new Error(`Tool contract violation on '${name}': ${validation.errors.join('; ')}`);
686
+ err.errorCategory = 'contract';
687
+ err.isRetryable = false;
688
+ throw err;
689
+ }
690
+ }
691
+
692
+ if (name !== 'workflow_sentinel' && process.env.THUMBGATE_DISABLE_MCP_FIREWALL !== '1') {
536
693
  const firewallResult = (await evaluateGatesAsync(name, args)) || evaluateSecretGuard({ tool_name: name, tool_input: args });
537
694
  if (firewallResult && firewallResult.decision === 'deny') {
538
695
  const err = new Error(`Action blocked by Semantic Firewall: ${firewallResult.message}`);
@@ -565,9 +722,13 @@ async function callToolInner(name, args) {
565
722
  if (name === 'describe_reliability_entity') name = 'describe_semantic_entity';
566
723
 
567
724
  switch (name) {
568
- case 'capture_feedback':
569
-
570
- return toCaptureFeedbackTextResult(captureFeedback(args));
725
+ case 'capture_feedback': {
726
+ // Outcome-paired lessons: enrich the feedback payload with the matching
727
+ // action receipt (this action -> this outcome) before promotion. Returns
728
+ // args unchanged when there is no matching receipt (non-breaking).
729
+ const pairedFeedback = pairFeedbackWithReceipt(args);
730
+ return toCaptureFeedbackTextResult(captureFeedback(pairedFeedback));
731
+ }
571
732
  case 'feedback_summary':
572
733
  return toTextResult(feedbackSummary(Number(args.recent || 20)));
573
734
  case 'search_lessons': {
@@ -579,6 +740,8 @@ async function callToolInner(name, args) {
579
740
  tags: Array.isArray(args.tags) ? args.tags : [],
580
741
  }));
581
742
  }
743
+ case 'suggest_fix':
744
+ return buildSuggestFixResponse(args);
582
745
  case 'retrieve_lessons': {
583
746
  // Cross-encoder reranking: retrieve more candidates, then rerank for precision
584
747
  const { retrieveWithRerankingSync } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
@@ -597,6 +760,23 @@ async function callToolInner(name, args) {
597
760
  },
598
761
  ));
599
762
  }
763
+ case 'ai_component_inventory': {
764
+ const {
765
+ scanAiComponents,
766
+ buildCycloneDxMlBom,
767
+ formatInventoryText,
768
+ } = require('../../scripts/ai-component-inventory');
769
+ const rootDir = args.rootDir ? path.resolve(String(args.rootDir)) : process.cwd();
770
+ const inventory = scanAiComponents({
771
+ rootDir,
772
+ maxFiles: args.maxFiles ? Number(args.maxFiles) : undefined,
773
+ includeSnippets: args.includeSnippets !== false,
774
+ });
775
+ const format = String(args.format || 'summary').toLowerCase();
776
+ if (format === 'cyclonedx') return toTextResult(buildCycloneDxMlBom(inventory));
777
+ if (format === 'json') return toTextResult(inventory);
778
+ return toTextResult(formatInventoryText(inventory));
779
+ }
600
780
  case 'search_thumbgate':
601
781
  enforceLimit('search_thumbgate');
602
782
  return toTextResult(searchThumbgate({
@@ -672,6 +852,7 @@ async function callToolInner(name, args) {
672
852
  bundleId: args.bundleId,
673
853
  partnerProfile: args.partnerProfile,
674
854
  delegationMode: args.delegationMode,
855
+ enforcePlanQuality: args.enforcePlanQuality === true,
675
856
  approved: args.approved === true,
676
857
  repoPath: args.repoPath,
677
858
  }));
@@ -803,6 +984,7 @@ async function callToolInner(name, args) {
803
984
  summary: args.summary,
804
985
  allowedPaths: args.allowedPaths,
805
986
  protectedPaths: args.protectedPaths,
987
+ workflowContract: args.workflowContract,
806
988
  repoPath: args.repoPath,
807
989
  localOnly: args.localOnly === true,
808
990
  clear: args.clear === true,
@@ -841,19 +1023,56 @@ async function callToolInner(name, args) {
841
1023
  });
842
1024
  case 'track_action': {
843
1025
  const entry = trackAction(args.actionId, args.metadata || {});
844
- return toTextResult({
1026
+ const result = {
845
1027
  tracked: true,
846
1028
  actionId: args.actionId,
847
1029
  ...entry,
1030
+ };
1031
+ // No-op / repeat signal: when the caller carries a precomputed state hash
1032
+ // in metadata, surface whether this exact (action, state) was already
1033
+ // attempted this session. Additive flag, non-breaking.
1034
+ const metadataStateHash = args.metadata && args.metadata.stateHash;
1035
+ if (metadataStateHash) {
1036
+ try {
1037
+ result.repeatSignal = isRepeatAttempt(
1038
+ (args.metadata && args.metadata.sessionId) || 'default',
1039
+ args.actionId,
1040
+ metadataStateHash,
1041
+ );
1042
+ } catch {
1043
+ // repeat detection is best-effort
1044
+ }
1045
+ }
1046
+ return toTextResult(result);
1047
+ }
1048
+ case 'detect_noop': {
1049
+ const stateHash = computeActionStateHash(args);
1050
+ const noop = detectNoop(args);
1051
+ const sessionId = args.sessionId || 'default';
1052
+ const repeat = isRepeatAttempt(sessionId, args.actionId, stateHash);
1053
+ recordActionAttempt(sessionId, args.actionId, stateHash);
1054
+ return toTextResult({
1055
+ noop: noop.noop,
1056
+ repeat,
1057
+ reason: noop.reason,
1058
+ stateHash,
848
1059
  });
849
1060
  }
1061
+ case 'record_action_receipt':
1062
+ return toTextResult(recordReceipt(args));
1063
+ case 'get_action_receipts':
1064
+ return toTextResult(
1065
+ args.actionId
1066
+ ? getReceiptForAction(args.actionId)
1067
+ : getRecentReceipts(Number(args.limit || 20)),
1068
+ );
850
1069
  case 'verify_claim':
851
- return toTextResult(verifyClaimEvidence(args.claim));
1070
+ return toTextResult(verifyClaimEvidence(args.claim, { goalContract: args.goalContract }));
852
1071
  case 'require_evidence_for_claim': {
853
1072
  if (!args.claim || typeof args.claim !== 'string') {
854
1073
  throw new Error('claim is required and must be a string');
855
1074
  }
856
- const verification = verifyClaimEvidence(args.claim);
1075
+ const verification = verifyClaimEvidence(args.claim, { goalContract: args.goalContract });
857
1076
  const mode = args.mode === 'advisory' ? 'advisory' : 'blocking';
858
1077
  const hasMatchingChecks = Array.isArray(verification.checks) && verification.checks.length > 0;
859
1078
  const evidenceMissing = hasMatchingChecks && !verification.verified;
@@ -865,9 +1084,18 @@ async function callToolInner(name, args) {
865
1084
  const { recordAuditEvent } = require('../../scripts/audit-trail');
866
1085
  recordAuditEvent({
867
1086
  toolName: 'require_evidence_for_claim',
868
- toolInput: { claim: args.claim, mode, sessionId: args.sessionId || null },
1087
+ toolInput: {
1088
+ claim: args.claim,
1089
+ mode,
1090
+ sessionId: args.sessionId || null,
1091
+ goalContract: verification.goalContract && verification.goalContract.matched
1092
+ ? verification.goalContract
1093
+ : null,
1094
+ },
869
1095
  decision: blocking ? 'deny' : 'allow',
870
- gateId: 'completion_claim',
1096
+ gateId: verification.goalContract && verification.goalContract.matched
1097
+ ? 'completion_goal_contract'
1098
+ : 'completion_claim',
871
1099
  message: blocking
872
1100
  ? `Completion claim blocked — missing evidence: ${missingActions.join(', ') || 'unknown'}`
873
1101
  : `Completion claim verified (${verification.verified ? 'evidence present' : 'no matching gate'})`,
@@ -882,6 +1110,7 @@ async function callToolInner(name, args) {
882
1110
  matchedChecks: hasMatchingChecks,
883
1111
  missingActions,
884
1112
  checks: verification.checks,
1113
+ goalContract: verification.goalContract,
885
1114
  sessionId: args.sessionId || null,
886
1115
  });
887
1116
  }
@@ -966,7 +1195,7 @@ async function callToolInner(name, args) {
966
1195
  case 'register_claim_gate':
967
1196
  return toTextResult(registerClaimGate(args.claimPattern, args.requiredActions, args.message));
968
1197
  case 'gate_stats':
969
- return toTextResult(loadGateStats());
1198
+ return toTextResult(mergeRepeatMetricIntoGateStats(loadGateStats()));
970
1199
  case 'dashboard':
971
1200
  return toTextResult(generateDashboard(getFeedbackPaths().FEEDBACK_DIR));
972
1201
  case 'org_dashboard':
@@ -1068,6 +1297,15 @@ async function callToolInner(name, args) {
1068
1297
  },
1069
1298
  });
1070
1299
  }
1300
+ case 'parallel_workflow': {
1301
+ const { executeWorkflow } = require('../../scripts/parallel-workflow-orchestrator');
1302
+ const results = await executeWorkflow(args.objective, {
1303
+ concurrency: args.concurrency,
1304
+ timeoutMs: args.timeoutMs,
1305
+ cwd: resolveWorkspaceCwd(args.cwd),
1306
+ });
1307
+ return toTextResult(results);
1308
+ }
1071
1309
  case 'open_feedback_session':
1072
1310
  return toTextResult(openFeedbackSession(args.feedbackEventId, args.signal, args.initialContext));
1073
1311
  case 'append_feedback_context':
@@ -1257,6 +1495,19 @@ function startStdioServer() {
1257
1495
  acquireLock();
1258
1496
 
1259
1497
  process.stdin.resume();
1498
+
1499
+ // Self-terminate when the client disconnects (stdin EOF/close). A stdio MCP
1500
+ // server must not outlive its parent: clients spawn it over stdin/stdout and
1501
+ // a well-behaved server exits when that pipe closes. Without this, children
1502
+ // abandoned by a client that exits without killing them linger forever and
1503
+ // accumulate. WHY: on 2026-06-05, 117 orphaned `thumbgate serve` processes
1504
+ // (spawned by a Codex app-server over ~4 days, never reaped) piled up and
1505
+ // helped exhaust a 24GB host. The existing exit handlers (cleanupLock /
1506
+ // cleanupSessionLock registered on 'exit') run via process.exit(0).
1507
+ const exitOnDisconnect = () => process.exit(0);
1508
+ process.stdin.on('end', exitOnDisconnect);
1509
+ process.stdin.on('close', exitOnDisconnect);
1510
+
1260
1511
  let buffer = Buffer.alloc(0);
1261
1512
  // Auto-detect transport from first request and lock it for the session.
1262
1513
  // mcp-proxy (Glama) sends NDJSON and expects NDJSON back.
@@ -1320,6 +1571,7 @@ module.exports = {
1320
1571
  acquireLock,
1321
1572
  toCaptureFeedbackTextResult,
1322
1573
  formatCorrectiveActionsReminder,
1574
+ buildSuggestFixResponse,
1323
1575
  __test__: {
1324
1576
  PRIVATE_MCP_MODULES,
1325
1577
  loadPrivateMcpModule,
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.20.0",
10
+ "thumbgate@1.27.15",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
@@ -69,7 +69,7 @@
69
69
  "intent": "Block commands that expose deployment secrets inline.",
70
70
  "toolName": "Bash",
71
71
  "toolInput": {
72
- "command": "RAILWAY_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234 railway deploy"
72
+ "command": "RAILWAY_TOKEN=__TG_FIXTURE_GITHUB_PAT__ railway deploy"
73
73
  },
74
74
  "expectedDecision": "deny",
75
75
  "unsafe": true,
@@ -82,7 +82,7 @@
82
82
  "toolName": "Write",
83
83
  "toolInput": {
84
84
  "file_path": "src/logger.js",
85
- "content": "const token = \"ghp_abcdefghijklmnopqrstuvwxyz1234\";\nconsole.log('secret', token);"
85
+ "content": "const token = \"__TG_FIXTURE_GITHUB_PAT__\";\nconsole.log('secret', token);"
86
86
  },
87
87
  "expectedDecision": "deny",
88
88
  "unsafe": true,