thumbgate 1.37.2 → 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 (40) 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/comet-session-logger/SKILL.md +59 -0
  4. package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
  5. package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
  6. package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
  7. package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  8. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +7 -3
  9. package/.claude-plugin/plugin.json +1 -1
  10. package/.well-known/mcp/server-card.json +1 -1
  11. package/adapters/claude/.mcp.json +2 -2
  12. package/adapters/forge/forge.yaml +3 -3
  13. package/adapters/future-agi/.mcp.json +1 -1
  14. package/adapters/future-agi/config.toml +1 -1
  15. package/adapters/future-agi/opencode.json +1 -1
  16. package/adapters/herdr/herdr-plugin.toml +2 -2
  17. package/adapters/mcp/server-stdio.js +1 -1
  18. package/adapters/opencode/opencode.json +1 -1
  19. package/bin/cli.js +132 -4
  20. package/config/gate-templates.json +48 -0
  21. package/package.json +36 -9
  22. package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
  23. package/public/index.html +2 -2
  24. package/public/install.html +7 -7
  25. package/public/numbers.html +2 -2
  26. package/scripts/ci-gha-buildkite-patterns.js +393 -0
  27. package/scripts/cli-schema.js +51 -1
  28. package/scripts/deeppattern-discipline-honesty.js +431 -0
  29. package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
  30. package/scripts/llm-obs-honesty.js +478 -0
  31. package/scripts/thumbgate-board-loop.js +432 -0
  32. package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
  33. package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
  34. package/scripts/typesafe-typed-questions.js +332 -7
  35. package/server.json +2 -2
  36. package/skills/article-syndication-pipeline/SKILL.md +37 -0
  37. package/skills/comet-session-logger/SKILL.md +59 -0
  38. package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  39. package/src/index.js +10 -0
  40. package/src/integrations/ideabrowser-connector.js +190 -0
@@ -16,20 +16,27 @@
16
16
  * 3. Code owns route() (pass | review | block) — the model does not emit the verdict
17
17
  * 4. Confidence is a second axis; low confidence on high-stakes fails closed
18
18
  *
19
- * Does NOT install typesafe-sdk, call api.typesafe.ai, clone Jev, or wire
20
- * an LLM adjudicator (#3690 / #3687, ECI pause). Deterministic matchers
21
- * answer the battery; existing PreToolUse / gate-check remains the enforcer.
19
+ * Does NOT install typesafe-sdk, clone Jev, or wire an LLM adjudicator
20
+ * (#3690 / #3687, ECI pause) as the PreToolUse gate. Deterministic matchers
21
+ * own route(). Optional `--live` shadows the same battery against
22
+ * POST /v1/systemone (Jev) for calibration — never as the verdict.
22
23
  */
23
24
 
24
25
  const fs = require('node:fs');
26
+ const os = require('node:os');
25
27
  const path = require('node:path');
26
28
 
27
29
  const SOURCE_URLS = Object.freeze([
28
30
  'https://console.typesafe.ai/hook',
31
+ 'https://docs.typesafe.ai/api',
29
32
  'https://docs.typesafe.ai/cookbooks/llm_guardrails.md',
30
33
  'https://docs.typesafe.ai/patterns/confidence-routing.md',
31
34
  ]);
32
35
 
36
+ const SYSTEMONE_URL = 'https://api.typesafe.ai/v1/systemone';
37
+ const PARALLEL_QUESTIONS_COOKBOOK = 'https://docs.typesafe.ai/cookbooks/parallel_questions';
38
+ const SHADOW_NOUL_DELTA = 0.4;
39
+
33
40
  const TYPED_KINDS = Object.freeze(['noul', 'choice', 'score']);
34
41
  const ROUTE_PRECEDENCE = Object.freeze(['block', 'review', 'pass']);
35
42
  const ROUTE_ACTIONS = Object.freeze(['block', 'review', 'pass']);
@@ -132,6 +139,7 @@ const DEFAULT_BATTERY = Object.freeze({
132
139
  });
133
140
 
134
141
  const RAIL_MAP = Object.freeze([
142
+ { typesafe: 'one POST with all N questions (parallel_questions cookbook)', thumbgate: '--live sends the whole battery once; --fan-out-questions is refused' },
135
143
  { typesafe: 'noul (one hazard each)', thumbgate: 'existing secret / destructive / outbound / tamper matchers' },
136
144
  { typesafe: 'choice over a closed set', thumbgate: 'hazard_family composed in code from which noul fired' },
137
145
  { typesafe: 'score rubric', thumbgate: 'severity 0–3 from the same matchers' },
@@ -441,11 +449,299 @@ function normalizeOptions(raw = {}) {
441
449
  useTypesafeApi: normalizeBoolean(raw['use-typesafe-api'] || raw.useTypesafeApi),
442
450
  llmAdjudicate: normalizeBoolean(raw['llm-adjudicate'] || raw.llmAdjudicate),
443
451
  modelEmittedVerdict: raw['model-emitted-verdict'] || raw.modelEmittedVerdict || null,
452
+ live: normalizeBoolean(raw.live),
453
+ fanOutQuestions: normalizeBoolean(raw['fan-out-questions'] || raw.fanOutQuestions),
454
+ apiKey: Object.prototype.hasOwnProperty.call(raw, 'apiKey')
455
+ ? raw.apiKey
456
+ : (raw['api-key'] || undefined),
457
+ fetchImpl: typeof raw.fetchImpl === 'function' ? raw.fetchImpl : undefined,
444
458
  strict: normalizeBoolean(raw.strict),
445
459
  json: normalizeBoolean(raw.json),
446
460
  };
447
461
  }
448
462
 
463
+ function loadTypesafeApiKey() {
464
+ const env = process.env.TYPESAFE_API_KEY;
465
+ if (env && String(env).trim()) return String(env).trim();
466
+ const fallback = path.join(os.homedir(), '.resume_secrets', 'TYPESAFE_API_KEY');
467
+ if (!fs.existsSync(fallback)) return null;
468
+ const text = fs.readFileSync(fallback, 'utf8').trim();
469
+ return text || null;
470
+ }
471
+
472
+ function parallelReceipt(questions, usage) {
473
+ const questionCount = Object.keys(questions || {}).length;
474
+ const inputTokens = usage && usage.input_tokens != null ? Number(usage.input_tokens) : null;
475
+ const estimatedFanoutInputTokens = inputTokens != null && questionCount > 0
476
+ ? inputTokens * questionCount
477
+ : null;
478
+ return {
479
+ cookbook: PARALLEL_QUESTIONS_COOKBOOK,
480
+ batchedCalls: 1,
481
+ questionCount,
482
+ estimatedFanoutCalls: questionCount,
483
+ inputTokens,
484
+ estimatedFanoutInputTokens,
485
+ estimatedTokenFactor: questionCount || null,
486
+ note: 'Estimated fan-out cost is N× this call\'s input tokens (document-dominated). Not a measured 12.2× cookbook number.',
487
+ };
488
+ }
489
+
490
+ function questionsForApi(battery) {
491
+ const out = {};
492
+ for (const [id, question] of Object.entries(battery || {})) {
493
+ if (!question || typeof question !== 'object') continue;
494
+ const type = String(question.type || '').toLowerCase();
495
+ if (!TYPED_KINDS.includes(type) || !question.instructions) continue;
496
+ const entry = { type, instructions: question.instructions };
497
+ if (question.criteria != null) entry.criteria = question.criteria;
498
+ out[id] = entry;
499
+ }
500
+ return out;
501
+ }
502
+
503
+ async function callSystemOne({
504
+ state,
505
+ questions,
506
+ apiKey,
507
+ model = 'jev-latest',
508
+ fetchImpl,
509
+ } = {}) {
510
+ if (!apiKey) {
511
+ const err = new Error('missing_api_key');
512
+ err.code = 'missing_api_key';
513
+ throw err;
514
+ }
515
+ const fetchFn = typeof fetchImpl === 'function' ? fetchImpl : globalThis.fetch;
516
+ if (typeof fetchFn !== 'function') {
517
+ const err = new Error('fetch_unavailable');
518
+ err.code = 'fetch_unavailable';
519
+ throw err;
520
+ }
521
+ const res = await fetchFn(SYSTEMONE_URL, {
522
+ method: 'POST',
523
+ headers: {
524
+ Authorization: `Bearer ${apiKey}`,
525
+ 'Content-Type': 'application/json',
526
+ },
527
+ body: JSON.stringify({ state, model, questions }),
528
+ });
529
+ const text = await res.text();
530
+ let body = null;
531
+ try {
532
+ body = text ? JSON.parse(text) : null;
533
+ } catch {
534
+ body = null;
535
+ }
536
+ if (!res.ok) {
537
+ const err = new Error(`systemone_http_${res.status}`);
538
+ err.code = res.status === 401 ? 'live_unauthorized' : 'live_http_error';
539
+ err.status = res.status;
540
+ throw err;
541
+ }
542
+ return body;
543
+ }
544
+
545
+ function compareShadow(detAnswers, jevAnswers) {
546
+ const divergences = [];
547
+ const jev = jevAnswers && typeof jevAnswers === 'object' ? jevAnswers : {};
548
+ for (const [id, det] of Object.entries(detAnswers || {})) {
549
+ const live = jev[id];
550
+ if (!live || !det) continue;
551
+ if (det.type === 'noul' && live.type === 'noul' && det.noul != null && live.noul != null) {
552
+ const delta = Math.abs(Number(live.noul) - Number(det.noul));
553
+ if (delta >= SHADOW_NOUL_DELTA) {
554
+ divergences.push({
555
+ id,
556
+ type: 'noul',
557
+ deterministic: Number(det.noul),
558
+ jev: Number(live.noul),
559
+ delta,
560
+ });
561
+ }
562
+ }
563
+ if (det.type === 'choice' && live.type === 'choice' && det.choice && live.choice && det.choice !== live.choice) {
564
+ divergences.push({
565
+ id,
566
+ type: 'choice',
567
+ deterministic: det.choice,
568
+ jev: live.choice,
569
+ });
570
+ }
571
+ if (det.type === 'score' && live.type === 'score' && det.score != null && live.score != null) {
572
+ const delta = Math.abs(Number(live.score) - Number(det.score));
573
+ if (delta >= 1) {
574
+ divergences.push({
575
+ id,
576
+ type: 'score',
577
+ deterministic: Number(det.score),
578
+ jev: Number(live.score),
579
+ delta,
580
+ });
581
+ }
582
+ }
583
+ }
584
+ return divergences;
585
+ }
586
+
587
+ function normalizeMetric(answer, question = {}) {
588
+ if (!answer || typeof answer !== 'object') return null;
589
+ const type = String(answer.type || (question && question.type) || '').toLowerCase();
590
+ if (type === 'noul') {
591
+ return answer.noul != null ? Number(answer.noul) : null;
592
+ }
593
+ if (type === 'choice') {
594
+ if (answer.probabilities && typeof answer.probabilities === 'object') {
595
+ const probs = Object.values(answer.probabilities).map(Number).filter((n) => !Number.isNaN(n));
596
+ if (probs.length) return Math.max(...probs);
597
+ }
598
+ return answer.confidence != null ? Number(answer.confidence) : 1.0;
599
+ }
600
+ if (type === 'score') {
601
+ const raw = answer.score != null ? Number(answer.score) : 0;
602
+ const criteria = question && Array.isArray(question.criteria) ? question.criteria : null;
603
+ const maxLevel = criteria && criteria.length > 1 ? criteria.length - 1 : 3;
604
+ return maxLevel > 0 ? Number((raw / maxLevel).toFixed(4)) : raw;
605
+ }
606
+ return null;
607
+ }
608
+
609
+ function computeVariance(runs) {
610
+ if (!Array.isArray(runs) || runs.length < 2) return {};
611
+ const stats = {};
612
+ const keys = Object.keys(runs[0] || {});
613
+ for (const key of keys) {
614
+ const values = runs.map((r) => Number(r[key])).filter((v) => !Number.isNaN(v));
615
+ if (values.length < 2) continue;
616
+ const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
617
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (values.length - 1);
618
+ const stdDev = Math.sqrt(variance);
619
+ stats[key] = {
620
+ mean: Number(mean.toFixed(4)),
621
+ stdDev: Number(stdDev.toFixed(4)),
622
+ runs: values.length,
623
+ zeroVariance: stdDev === 0,
624
+ };
625
+ }
626
+ return stats;
627
+ }
628
+
629
+ async function attachLiveShadow(report, options = {}) {
630
+ if (options.fanOutQuestions) {
631
+ report.findings.push({
632
+ id: 'parallel_fanout_refused',
633
+ severity: 'fail',
634
+ gateId: 'require-typed-pretool-questions',
635
+ message: 'Refused --fan-out-questions. Parallel-questions cookbook: one POST with all N questions, not N calls. Do not re-pay the document N times.',
636
+ });
637
+ report.status = 'fail';
638
+ report.ok = false;
639
+ report.parallel = {
640
+ cookbook: PARALLEL_QUESTIONS_COOKBOOK,
641
+ batchedCalls: 0,
642
+ refusedFanOut: true,
643
+ };
644
+ return report;
645
+ }
646
+ if (!options.live || options.mapOnly || options.useTypesafeApi) return report;
647
+ const apiKey = options.apiKey !== undefined ? options.apiKey : loadTypesafeApiKey();
648
+ if (!apiKey) {
649
+ report.findings.push({
650
+ id: 'live_key_missing',
651
+ severity: 'fail',
652
+ gateId: 'require-code-owned-route',
653
+ message: '--live needs TYPESAFE_API_KEY (env or ~/.resume_secrets/TYPESAFE_API_KEY). Key is never printed.',
654
+ });
655
+ report.status = 'fail';
656
+ report.ok = false;
657
+ report.shadow = { used: false, reason: 'missing_api_key' };
658
+ return report;
659
+ }
660
+
661
+ const battery = parseBattery(options.batteryText || '').battery || DEFAULT_BATTERY;
662
+ const questions = questionsForApi(battery);
663
+ try {
664
+ const body = await callSystemOne({
665
+ state: options.payloadText
666
+ ? parsePayload(options.payloadText).payload
667
+ : {
668
+ tool_name: options.toolName,
669
+ tool_input: { command: options.command },
670
+ },
671
+ questions,
672
+ apiKey,
673
+ fetchImpl: options.fetchImpl,
674
+ });
675
+ const jevAnswers = (body && body.answers) || {};
676
+ const divergences = compareShadow(report.answers, jevAnswers);
677
+ report.parallel = parallelReceipt(questions, body && body.usage);
678
+ report.shadow = {
679
+ used: true,
680
+ ownsRoute: false,
681
+ model: body && body.model,
682
+ usage: body && body.usage,
683
+ answers: jevAnswers,
684
+ divergences,
685
+ };
686
+ for (const d of divergences) {
687
+ report.findings.push({
688
+ id: 'shadow_divergence',
689
+ severity: 'warn',
690
+ gateId: 'require-typed-pretool-questions',
691
+ questionId: d.id,
692
+ message: `Jev shadow disagrees with deterministic ${d.type} on ${d.id} (code still owns route=${report.route}).`,
693
+ });
694
+ }
695
+ if (divergences.length && report.status === 'ready') report.status = 'actionable';
696
+ } catch (err) {
697
+ const id = err && err.code === 'live_unauthorized' ? 'live_unauthorized' : 'live_http_error';
698
+ const status = err && err.status ? Number(err.status) : null;
699
+ const raw = String((err && err.message) || '');
700
+ const safeMsg = /apikey_|bearer\s/i.test(raw) ? 'redacted' : raw.slice(0, 160);
701
+ const errorCode = err && (err.code || (err.cause && err.cause.code));
702
+ report.findings.push({
703
+ id,
704
+ severity: 'fail',
705
+ gateId: 'require-code-owned-route',
706
+ message: `Live System One call failed (HTTP ${status || 'unknown'}${safeMsg ? `: ${safeMsg}` : ''}). Route stays deterministic. Secret is not logged.`,
707
+ });
708
+ report.status = 'fail';
709
+ report.ok = false;
710
+ report.shadow = {
711
+ used: false,
712
+ reason: id,
713
+ status,
714
+ errorName: err && err.name,
715
+ errorCode: errorCode || null,
716
+ };
717
+ }
718
+ return report;
719
+ }
720
+
721
+ async function buildTypesafeTypedQuestionsReportAsync(rawOptions = {}) {
722
+ const report = buildTypesafeTypedQuestionsReport(rawOptions);
723
+ const options = normalizeOptions(rawOptions);
724
+ if (options.fanOutQuestions) return report;
725
+ if (!options.live || options.mapOnly) return report;
726
+ const payloadText = options.payloadText || JSON.stringify({
727
+ tool_name: options.toolName,
728
+ tool_input: { command: options.command },
729
+ });
730
+ await attachLiveShadow(report, {
731
+ live: true,
732
+ mapOnly: false,
733
+ useTypesafeApi: options.useTypesafeApi,
734
+ fanOutQuestions: options.fanOutQuestions,
735
+ apiKey: options.apiKey,
736
+ fetchImpl: options.fetchImpl,
737
+ batteryText: options.batteryText,
738
+ payloadText,
739
+ toolName: options.toolName,
740
+ command: options.command,
741
+ });
742
+ return report;
743
+ }
744
+
449
745
  function buildFindings({
450
746
  batteryParse,
451
747
  payloadParse,
@@ -506,6 +802,14 @@ function buildFindings({
506
802
  message: 'Refused --use-typesafe-api. Do not call api.typesafe.ai from PreToolUse (ECI; LLM adjudicator parked).',
507
803
  });
508
804
  }
805
+ if (flags.fanOutQuestions) {
806
+ findings.push({
807
+ id: 'parallel_fanout_refused',
808
+ severity: 'fail',
809
+ gateId: 'require-typed-pretool-questions',
810
+ message: 'Refused --fan-out-questions. Parallel-questions cookbook: one POST with all N questions, not N calls.',
811
+ });
812
+ }
509
813
  if (flags.llmAdjudicate) {
510
814
  findings.push({
511
815
  id: 'llm_adjudicator_parked',
@@ -587,6 +891,7 @@ function buildTypesafeTypedQuestionsReport(rawOptions = {}) {
587
891
  useTypesafeApi: options.useTypesafeApi,
588
892
  llmAdjudicate: options.llmAdjudicate,
589
893
  modelEmittedVerdict: options.modelEmittedVerdict,
894
+ fanOutQuestions: options.fanOutQuestions,
590
895
  };
591
896
 
592
897
  const evaluated = options.mapOnly
@@ -745,6 +1050,8 @@ function parseCliArgs(argv) {
745
1050
  if (arg === '--clone-jev') { options['clone-jev'] = true; continue; }
746
1051
  if (arg === '--use-typesafe-api') { options['use-typesafe-api'] = true; continue; }
747
1052
  if (arg === '--llm-adjudicate') { options['llm-adjudicate'] = true; continue; }
1053
+ if (arg === '--live') { options.live = true; continue; }
1054
+ if (arg === '--fan-out-questions') { options['fan-out-questions'] = true; continue; }
748
1055
  if (arg === '--help' || arg === '-h') { options.help = true; continue; }
749
1056
  const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
750
1057
  if (!m) continue;
@@ -765,7 +1072,9 @@ Flags:
765
1072
  --map-only Print TypeSafe → ThumbGate rail map
766
1073
  --claim-ready Fail unless code owns the route
767
1074
  --clone-jev Always fail (SKU clone)
768
- --use-typesafe-api Always fail (do not call Jev)
1075
+ --use-typesafe-api Always fail (Jev as the PreToolUse gate)
1076
+ --live Shadow the battery against api.typesafe.ai; one POST, all questions
1077
+ --fan-out-questions Always fail (cookbook: do not pay the document N times)
769
1078
  --llm-adjudicate Always fail (#3690/#3687 parked)
770
1079
  --model-emitted-verdict=V Always fail (code must own route)
771
1080
  --root=DIR Repo root for relative paths
@@ -776,13 +1085,13 @@ Sources: ${SOURCE_URLS.join(' ')}
776
1085
  `);
777
1086
  }
778
1087
 
779
- function runCli(argv = process.argv.slice(2)) {
1088
+ async function runCli(argv = process.argv.slice(2)) {
780
1089
  const args = parseCliArgs(argv);
781
1090
  if (args.help) {
782
1091
  printHelp();
783
1092
  return 0;
784
1093
  }
785
- const report = buildTypesafeTypedQuestionsReport(args);
1094
+ const report = await buildTypesafeTypedQuestionsReportAsync(args);
786
1095
  if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
787
1096
  else process.stdout.write(formatTypesafeTypedQuestionsReport(report));
788
1097
  if (args.strict && report.status !== 'ready') return 1;
@@ -803,11 +1112,27 @@ module.exports = {
803
1112
  composeHazardFamily,
804
1113
  composeSeverity,
805
1114
  route,
1115
+ SYSTEMONE_URL,
1116
+ loadTypesafeApiKey,
1117
+ questionsForApi,
1118
+ callSystemOne,
1119
+ compareShadow,
1120
+ attachLiveShadow,
1121
+ parallelReceipt,
1122
+ PARALLEL_QUESTIONS_COOKBOOK,
1123
+ normalizeMetric,
1124
+ computeVariance,
806
1125
  buildTypesafeTypedQuestionsReport,
1126
+ buildTypesafeTypedQuestionsReportAsync,
807
1127
  formatTypesafeTypedQuestionsReport,
808
1128
  runCli,
809
1129
  };
810
1130
 
811
1131
  if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
812
- process.exitCode = runCli();
1132
+ runCli().then((code) => {
1133
+ process.exitCode = code;
1134
+ }).catch((err) => {
1135
+ process.stderr.write(`${err && err.message ? err.message : err}\n`);
1136
+ process.exitCode = 1;
1137
+ });
813
1138
  }
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
10
  },
11
- "version": "1.37.2",
11
+ "version": "1.37.3",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "thumbgate",
17
- "version": "1.37.2",
17
+ "version": "1.37.3",
18
18
  "runtimeHint": "npx",
19
19
  "runtimeArguments": [
20
20
  {
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: article-syndication-pipeline
3
+ description: Technical article syndication pipeline for cross-posting Dev.to articles to CoderLegion and other developer platforms with canonical URL preservation, UTM parameter attribution, and contributor onboarding compliance.
4
+ ---
5
+
6
+ # ✍️ Article Syndication & Contributor Pipeline
7
+
8
+ This skill guides the autonomous syndication of ThumbGate technical discoveries, security postmortems, and engineering deep dives across developer platforms (Dev.to, CoderLegion, Hashnode, Medium).
9
+
10
+ ---
11
+
12
+ ## 🎯 Architecture & Operating Directives
13
+
14
+ 1. **Canonical SEO Integrity:**
15
+ - Always preserve the primary origin as the canonical URL (e.g. `https://dev.to/igorganapolsky/...`).
16
+ - Syndicated copies on CoderLegion or elsewhere must link back to avoid search engine duplicate-content penalties.
17
+ 2. **First-Party Revenue Attribution:**
18
+ - All inbound links to `thumbgate.ai` must include UTM campaign tags (`utm_source=coderlegion`, `utm_medium=community`, `utm_campaign=guest-post`).
19
+ - Conversion links (Pro license checkout) must route through `/go/pro` (`https://thumbgate.ai/go/pro?utm_source=...`) to ensure server-side redirect logging.
20
+ 3. **CoderLegion Contributor Compliance:**
21
+ - URL import endpoint: `https://coderlegion.com/import-post`.
22
+ - Supports 2 imported Dev.to articles with full attribution.
23
+ - Contributor checklist: `https://coderlegion.com/guest-blog-checklists`.
24
+ - All imported posts undergo editorial review before appearing on the main feed.
25
+
26
+ ---
27
+
28
+ ## 🛠️ Step-by-Step Workflow
29
+
30
+ 1. **Verify Original Article:**
31
+ Verify original URL exists, returns HTTP 200, and has correct canonical tags.
32
+ 2. **Fetch via Platform Parser:**
33
+ Submit `import_url` to `https://coderlegion.com/import-post`.
34
+ 3. **Review & Confirm Metadata:**
35
+ Confirm title, tags (`ai`, `security`, `architecture`, `opensource`), and backlink to canonical source.
36
+ 4. **Publish & Track:**
37
+ Submit publication step and record post URL in session log and Comet browser tab.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: comet-session-logger
3
+ description: Live session logger and observability dashboard driver for Comet browser. Ensures every PR, deployment status check, CoderLegion/Dev.to publication, and Railway production health metric is actively logged and visualized in Comet tabs.
4
+ ---
5
+
6
+ # ☄️ Comet Session Logger & Visual HUD
7
+
8
+ This skill governs the automated logging, tab orchestration, and visual tracking of all ThumbGate agentic tasks inside the **Comet** browser on macOS.
9
+
10
+ ---
11
+
12
+ ## 🎯 Purpose & Directives
13
+
14
+ 1. **CEO Visual Observability:** The CEO monitors agent progress via open tabs in Comet. Every major milestone must be reflected in Comet tabs without disrupting existing work.
15
+ 2. **Deterministic Tab Management:** Uses macOS AppleScript (`osascript`) to inspect, activate, create, or update Comet tabs.
16
+ 3. **Fail-Safe & Non-Destructive:** Never closes existing tabs; always reuses matching URLs or opens clean dedicated monitoring tabs.
17
+
18
+ ---
19
+
20
+ ## 🧭 Standard Tab Targets
21
+
22
+ | Surface | Target URL | Trigger Milestone |
23
+ |---------|------------|-------------------|
24
+ | **Pull Requests** | `https://github.com/IgorGanapolsky/ThumbGate/pull/<NUM>` | After creating or advancing a PR |
25
+ | **CI / Checks** | `https://github.com/IgorGanapolsky/ThumbGate/actions` | During workflow verification |
26
+ | **Production Health** | `https://thumbgate-production.up.railway.app/health` | After deployment gate verification |
27
+ | **Railway Console** | `https://railway.com/project/thumbgate-production` | When inspecting infrastructure |
28
+ | **CoderLegion** | `https://coderlegion.com/import-post` | When onboarding or syndicating articles |
29
+ | **Dev.to Dashboard** | `https://dev.to/dashboard` | When tracking article reach & analytics |
30
+
31
+ ---
32
+
33
+ ## 🛠️ AppleScript Orchestration Helpers
34
+
35
+ ```bash
36
+ # Check existing Comet tabs
37
+ osascript -e '
38
+ tell application "Comet"
39
+ set outList to ""
40
+ repeat with w in windows
41
+ repeat with t in tabs of w
42
+ set outList to outList & (URL of t) & " | " & (title of t) & linefeed
43
+ end repeat
44
+ end repeat
45
+ return outList
46
+ end tell'
47
+
48
+ # Open or focus a specific URL in Comet
49
+ open_comet_url() {
50
+ local target_url="$1"
51
+ osascript -e "
52
+ tell application \"Comet\"
53
+ activate
54
+ tell front window
55
+ make new tab with properties {URL:\"${target_url}\"}
56
+ end tell
57
+ end tell"
58
+ }
59
+ ```
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: thumbgate-daily-discoveries
3
+ description: Autonomous daily publishing engine and monitoring skill for ThumbGate news and technical discoveries. Generates high-density engineering posts with full UTM attribution, synchronizes to Obsidian, schedules dual cron/launchd runs at 9:00 AM EST, and tracks publication ledgers.
4
+ ---
5
+
6
+ # 📰 ThumbGate Daily Technical Discoveries & News Engine
7
+
8
+ This skill automates the daily harvesting, synthesis, UTM attribution, and multi-channel publication of ThumbGate engineering discoveries and release news.
9
+
10
+ ---
11
+
12
+ ## 🎯 Architecture & Operating Principles
13
+
14
+ 1. **Daily Cadence (9:00 AM EST):** Runs once per calendar day. Avoids spam while establishing consistent technical authority.
15
+ 2. **Dual Scheduling Redundancy:**
16
+ - Primary: macOS crontab (`0 9 * * * /bin/bash scripts/thumbgate-daily-discoveries-publish.sh ...`)
17
+ - Fallback: macOS LaunchAgent (`~/Library/LaunchAgents/com.thumbgate.daily-publish.plist`)
18
+ - Note: GitHub Actions `schedule:` is strictly limited to `codeql.yml` per repo hygiene tests.
19
+ 3. **Full UTM Attribution & Tracked Redirects:**
20
+ - All outbound links to `thumbgate.ai` must use `buildUTMLink` from `scripts/social-analytics/utm.js`.
21
+ - Outbound pro/checkout links must route through `/go/pro` (`serveTrackedLinkRedirect` in `src/api/server.js`) to capture first-party funnel telemetry.
22
+ 4. **Multi-Surface Staging & Sync:**
23
+ - Staged locally at `docs/marketing/daily-discoveries/YYYY-MM-DD-<slug>.md`.
24
+ - Synchronized to the CEO's Obsidian Vault at `~/Documents/Igor/Research/Daily-Discoveries/`.
25
+ - Logged idempotently to `.thumbgate/daily-discoveries-ledger.jsonl`.
26
+ - Dispatches to Dev.to / Zernio if API keys are configured.
27
+
28
+ ---
29
+
30
+ ## 🛠️ CLI Commands & Usage
31
+
32
+ ```bash
33
+ # Preview today's technical discovery without making mutations or network calls
34
+ node scripts/thumbgate-daily-discoveries-publish.js --dry-run
35
+
36
+ # Output structured JSON report
37
+ node scripts/thumbgate-daily-discoveries-publish.js --json
38
+
39
+ # Force publish/restage today's discovery even if already logged in ledger
40
+ node scripts/thumbgate-daily-discoveries-publish.js --force
41
+
42
+ # Shell wrapper (used by crontab / launchd)
43
+ ./scripts/thumbgate-daily-discoveries-publish.sh
44
+
45
+ # Run test suite
46
+ node --test tests/thumbgate-daily-discoveries-publish.test.js
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 📋 Pre-Flight Verification Checklist
52
+
53
+ - [ ] Verify `node --test tests/thumbgate-daily-discoveries-publish.test.js` passes.
54
+ - [ ] Verify outbound links contain valid UTM parameters (`utm_source=daily_discoveries`, `utm_campaign=daily_technical_discoveries`).
55
+ - [ ] Verify `launchctl list | grep thumbgate` reports exit code 0.
56
+ - [ ] Verify `crontab -l | grep thumbgate` contains the 9:00 AM daily trigger.
57
+ - [ ] Check `.thumbgate/daily-discoveries-ledger.jsonl` for recent execution receipts.
package/src/index.js CHANGED
@@ -12,6 +12,12 @@ const {
12
12
  isNamedIdentity,
13
13
  } = require('./hermes-platform-protocol');
14
14
  const { HermesSyncPlane } = require('./hermes-sync-plane');
15
+ const {
16
+ IdeaBrowserConnector,
17
+ ToolTier,
18
+ DEFAULT_READ_ONLY_TOOLS,
19
+ DEFAULT_MUTATING_TOOLS,
20
+ } = require('./integrations/ideabrowser-connector');
15
21
 
16
22
  // The package entry point is the production surface for `thumbgate`.
17
23
  // The hosted-Hermes identity/lifecycle/approval gates are exported here so
@@ -29,4 +35,8 @@ module.exports = Object.assign({}, feedbackLoop, {
29
35
  THREAD_MODES,
30
36
  FORBIDDEN_IDENTITIES,
31
37
  isNamedIdentity,
38
+ IdeaBrowserConnector,
39
+ ToolTier,
40
+ DEFAULT_READ_ONLY_TOOLS,
41
+ DEFAULT_MUTATING_TOOLS,
32
42
  });