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.
- package/.agents/skills/article-syndication-pipeline/SKILL.md +37 -0
- package/.agents/skills/ci-gha-buildkite-patterns-not-clone/SKILL.md +72 -0
- package/.agents/skills/comet-session-logger/SKILL.md +59 -0
- package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
- package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
- package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
- package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
- package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +7 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +1 -1
- package/adapters/future-agi/config.toml +1 -1
- package/adapters/future-agi/opencode.json +1 -1
- package/adapters/herdr/herdr-plugin.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +132 -4
- package/config/gate-templates.json +48 -0
- package/package.json +36 -9
- package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
- package/public/index.html +2 -2
- package/public/install.html +7 -7
- package/public/numbers.html +2 -2
- package/scripts/ci-gha-buildkite-patterns.js +393 -0
- package/scripts/cli-schema.js +51 -1
- package/scripts/deeppattern-discipline-honesty.js +431 -0
- package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
- package/scripts/llm-obs-honesty.js +478 -0
- package/scripts/thumbgate-board-loop.js +432 -0
- package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
- package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
- package/scripts/typesafe-typed-questions.js +332 -7
- package/server.json +2 -2
- package/skills/article-syndication-pipeline/SKILL.md +37 -0
- package/skills/comet-session-logger/SKILL.md +59 -0
- package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
- package/src/index.js +10 -0
- 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,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
});
|