thumbgate 1.27.18 → 1.27.20
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/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tool Contract Validator
|
|
5
|
+
* Validates tool arguments against the tool's inputSchema.
|
|
6
|
+
*/
|
|
7
|
+
function validateToolContract(schema, args) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
if (!schema) return { valid: true, errors };
|
|
10
|
+
|
|
11
|
+
if (schema.type === 'object') {
|
|
12
|
+
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
13
|
+
errors.push(`Expected object, got ${args === null ? 'null' : Array.isArray(args) ? 'array' : typeof args}`);
|
|
14
|
+
return { valid: false, errors };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check required fields
|
|
18
|
+
if (Array.isArray(schema.required)) {
|
|
19
|
+
for (const req of schema.required) {
|
|
20
|
+
if (args[req] === undefined || args[req] === null || args[req] === '') {
|
|
21
|
+
errors.push(`Missing required parameter: '${req}'`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check properties
|
|
27
|
+
if (schema.properties) {
|
|
28
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
29
|
+
const value = args[key];
|
|
30
|
+
if (value === undefined || value === null) continue; // Optional field not provided
|
|
31
|
+
|
|
32
|
+
const valType = typeof value;
|
|
33
|
+
if (propSchema.type === 'string') {
|
|
34
|
+
if (valType !== 'string') {
|
|
35
|
+
errors.push(`Parameter '${key}' must be a string (got ${valType})`);
|
|
36
|
+
} else if (Array.isArray(propSchema.enum)) {
|
|
37
|
+
if (!propSchema.enum.includes(value)) {
|
|
38
|
+
errors.push(`Parameter '${key}' must be one of [${propSchema.enum.join(', ')}] (got '${value}')`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else if (propSchema.type === 'number') {
|
|
42
|
+
if (valType !== 'number' || isNaN(value)) {
|
|
43
|
+
errors.push(`Parameter '${key}' must be a number (got ${valType})`);
|
|
44
|
+
}
|
|
45
|
+
} else if (propSchema.type === 'boolean') {
|
|
46
|
+
if (valType !== 'boolean') {
|
|
47
|
+
errors.push(`Parameter '${key}' must be a boolean (got ${valType})`);
|
|
48
|
+
}
|
|
49
|
+
} else if (propSchema.type === 'array') {
|
|
50
|
+
if (!Array.isArray(value)) {
|
|
51
|
+
errors.push(`Parameter '${key}' must be an array (got ${valType})`);
|
|
52
|
+
}
|
|
53
|
+
} else if (propSchema.type === 'object') {
|
|
54
|
+
if (valType !== 'object' || value === null || Array.isArray(value)) {
|
|
55
|
+
errors.push(`Parameter '${key}' must be an object (got ${valType})`);
|
|
56
|
+
} else {
|
|
57
|
+
// Recurse for nested objects
|
|
58
|
+
const subRes = validateToolContract(propSchema, value);
|
|
59
|
+
if (!subRes.valid) {
|
|
60
|
+
for (const err of subRes.errors) {
|
|
61
|
+
errors.push(`Parameter '${key}': ${err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
valid: errors.length === 0,
|
|
72
|
+
errors,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { validateToolContract };
|
package/scripts/tool-registry.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// Human-readable display title from a snake_case tool name.
|
|
5
|
+
// e.g. "capture_feedback" -> "Capture Feedback". The Claude Connectors Directory
|
|
6
|
+
// requires every tool to carry BOTH a title and a readOnlyHint/destructiveHint.
|
|
7
|
+
function humanizeTitle(name) {
|
|
8
|
+
return String(name || '')
|
|
9
|
+
.split(/[_-]+/)
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
12
|
+
.join(' ');
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
function readOnlyTool(tool) {
|
|
5
16
|
return {
|
|
6
17
|
...tool,
|
|
18
|
+
title: tool.title || humanizeTitle(tool.name),
|
|
7
19
|
annotations: {
|
|
20
|
+
title: tool.title || humanizeTitle(tool.name),
|
|
8
21
|
readOnlyHint: true,
|
|
9
22
|
},
|
|
10
23
|
};
|
|
@@ -13,12 +26,40 @@ function readOnlyTool(tool) {
|
|
|
13
26
|
function destructiveTool(tool) {
|
|
14
27
|
return {
|
|
15
28
|
...tool,
|
|
29
|
+
title: tool.title || humanizeTitle(tool.name),
|
|
16
30
|
annotations: {
|
|
31
|
+
title: tool.title || humanizeTitle(tool.name),
|
|
17
32
|
destructiveHint: true,
|
|
18
33
|
},
|
|
19
34
|
};
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
const GOAL_CONTRACT_SCHEMA = {
|
|
38
|
+
type: 'object',
|
|
39
|
+
description: 'Optional agent handoff contract. Use this when a worker/orchestrator/reviewer loop needs explicit done criteria before a done/fixed/shipped claim is allowed.',
|
|
40
|
+
properties: {
|
|
41
|
+
goal: { type: 'string', description: 'The operator-visible goal this claim is trying to close.' },
|
|
42
|
+
doneWhen: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: { type: 'string' },
|
|
45
|
+
description: 'Human-readable acceptance criteria for the task.',
|
|
46
|
+
},
|
|
47
|
+
proveBy: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: { type: 'string' },
|
|
50
|
+
description: 'Tracked action ids required before the claim can pass, such as tests_passed, review_completed, ci_green, deploy_verified, or operator_approved.',
|
|
51
|
+
},
|
|
52
|
+
mustNotChange: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
items: { type: 'string' },
|
|
55
|
+
description: 'Protected areas or constraints the agents must preserve while completing the goal.',
|
|
56
|
+
},
|
|
57
|
+
workerAgent: { type: 'string', description: 'Agent responsible for implementation.' },
|
|
58
|
+
reviewerAgent: { type: 'string', description: 'Agent responsible for independent verification.' },
|
|
59
|
+
orchestratorAgent: { type: 'string', description: 'Agent responsible for routing and deciding whether done can be claimed.' },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
22
63
|
const TOOLS = [
|
|
23
64
|
readOnlyTool({
|
|
24
65
|
name: 'capture_feedback',
|
|
@@ -120,6 +161,19 @@ const TOOLS = [
|
|
|
120
161
|
required: ['toolName'],
|
|
121
162
|
},
|
|
122
163
|
}),
|
|
164
|
+
readOnlyTool({
|
|
165
|
+
name: 'ai_component_inventory',
|
|
166
|
+
description: 'Scan a project for AI/ML provider SDKs, agent frameworks, vector databases, Vertex/Gemini/Dialogflow CX usage, and model artifacts. Returns evidence suitable for enterprise AI inventory and ML-BOM review.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
rootDir: { type: 'string', description: 'Project root to scan. Defaults to the current process working directory.' },
|
|
171
|
+
format: { type: 'string', enum: ['summary', 'json', 'cyclonedx'], description: 'Response format. summary is compact text; json returns ThumbGate inventory; cyclonedx returns ML-BOM JSON.' },
|
|
172
|
+
maxFiles: { type: 'number', description: 'Maximum files to scan (default 2500).' },
|
|
173
|
+
includeSnippets: { type: 'boolean', description: 'Include matched source snippets in evidence. Defaults true.' },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
123
177
|
readOnlyTool({
|
|
124
178
|
name: 'search_thumbgate',
|
|
125
179
|
description: 'Search raw ThumbGate state across feedback logs, ContextFS memory, prevention rules, and imported policy documents.',
|
|
@@ -371,6 +425,24 @@ const TOOLS = [
|
|
|
371
425
|
},
|
|
372
426
|
},
|
|
373
427
|
}),
|
|
428
|
+
readOnlyTool({
|
|
429
|
+
name: 'suggest_fix',
|
|
430
|
+
description: 'Suggest corrective actions for a described failure by searching the lesson DB and prevention rules. Returns up to 3 ranked suggestions with their source. Call this when something goes wrong and you need guidance on what to do next.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
required: ['context'],
|
|
434
|
+
properties: {
|
|
435
|
+
context: {
|
|
436
|
+
type: 'string',
|
|
437
|
+
description: 'Description of what went wrong or what the agent is trying to fix.',
|
|
438
|
+
},
|
|
439
|
+
limit: {
|
|
440
|
+
type: 'number',
|
|
441
|
+
description: 'Maximum number of suggestions to return (default 3, max 5).',
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
374
446
|
readOnlyTool({
|
|
375
447
|
name: 'infer_lesson_from_history',
|
|
376
448
|
description: 'Perform autonomous inference on chat history to identify why a failure occurred and what rule should be recorded.',
|
|
@@ -417,6 +489,7 @@ const TOOLS = [
|
|
|
417
489
|
bundleId: { type: 'string' },
|
|
418
490
|
partnerProfile: { type: 'string' },
|
|
419
491
|
delegationMode: { type: 'string', enum: ['off', 'auto', 'sequential'] },
|
|
492
|
+
enforcePlanQuality: { type: 'boolean' },
|
|
420
493
|
approved: { type: 'boolean' },
|
|
421
494
|
repoPath: { type: 'string' },
|
|
422
495
|
},
|
|
@@ -758,6 +831,17 @@ const TOOLS = [
|
|
|
758
831
|
items: { type: 'string' },
|
|
759
832
|
description: 'Optional protected-file globs that require explicit approval before editing or publishing',
|
|
760
833
|
},
|
|
834
|
+
workflowContract: {
|
|
835
|
+
type: 'object',
|
|
836
|
+
description: 'Optional deterministic workflow run contract. Supports workflowId, allowedBranches, blockedActions, requiredEvidence, and completionGate.',
|
|
837
|
+
properties: {
|
|
838
|
+
workflowId: { type: 'string' },
|
|
839
|
+
allowedBranches: { type: 'array', items: { type: 'string' } },
|
|
840
|
+
blockedActions: { type: 'array', items: { type: 'string' } },
|
|
841
|
+
requiredEvidence: { type: 'array', items: { type: 'string' } },
|
|
842
|
+
completionGate: { type: 'string' },
|
|
843
|
+
},
|
|
844
|
+
},
|
|
761
845
|
repoPath: { type: 'string', description: 'Optional repo root used when evaluating git diff scope' },
|
|
762
846
|
localOnly: { type: 'boolean', description: 'When true, also marks the task as local-only' },
|
|
763
847
|
clear: { type: 'boolean', description: 'Clear the current task scope instead of setting one' },
|
|
@@ -835,6 +919,56 @@ const TOOLS = [
|
|
|
835
919
|
},
|
|
836
920
|
},
|
|
837
921
|
}),
|
|
922
|
+
destructiveTool({
|
|
923
|
+
name: 'detect_noop',
|
|
924
|
+
title: 'Detect No-op Action',
|
|
925
|
+
description: 'Detect whether a tool call was a no-op (state unchanged) or identical to a prior attempt in the session — a cheap repeat-loop signal. Records the action attempt state for repeat detection.',
|
|
926
|
+
inputSchema: {
|
|
927
|
+
type: 'object',
|
|
928
|
+
required: ['actionId'],
|
|
929
|
+
properties: {
|
|
930
|
+
actionId: { type: 'string', description: 'Stable identifier for the action being checked (e.g. the file path or command being attempted)' },
|
|
931
|
+
kind: { type: 'string', enum: ['file', 'command'], description: 'Action kind: file edit/write or command execution' },
|
|
932
|
+
filePath: { type: 'string', description: 'Path of the file the action targets (file kind)' },
|
|
933
|
+
beforeContent: { type: 'string', description: 'File content before the action (file kind)' },
|
|
934
|
+
afterContent: { type: 'string', description: 'File content after the action (file kind)' },
|
|
935
|
+
exitCode: { type: 'number', description: 'Command exit code (command kind)' },
|
|
936
|
+
stdout: { type: 'string', description: 'Command stdout (command kind)' },
|
|
937
|
+
stderr: { type: 'string', description: 'Command stderr (command kind)' },
|
|
938
|
+
sessionId: { type: 'string', description: 'Optional session id used to scope repeat-attempt detection' },
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
}),
|
|
942
|
+
destructiveTool({
|
|
943
|
+
name: 'record_action_receipt',
|
|
944
|
+
title: 'Record Action Receipt',
|
|
945
|
+
description: 'Pair a tracked tool call with its outcome (diff, exit code, test result) so a promoted lesson encodes "this action -> this outcome", not just a thumbs signal. Appends to the action-receipts log.',
|
|
946
|
+
inputSchema: {
|
|
947
|
+
type: 'object',
|
|
948
|
+
required: ['actionId'],
|
|
949
|
+
properties: {
|
|
950
|
+
actionId: { type: 'string', description: 'Identifier of the tracked action this receipt pairs with' },
|
|
951
|
+
toolName: { type: 'string', description: 'Name of the tool that was invoked' },
|
|
952
|
+
toolInput: { type: 'object', description: 'Structured input the tool was called with' },
|
|
953
|
+
diff: { type: 'string', description: 'Optional unified diff or change summary produced by the action' },
|
|
954
|
+
exitCode: { type: 'number', description: 'Optional command exit code outcome' },
|
|
955
|
+
testOutcome: { type: 'string', description: 'Optional test outcome (e.g. passed, failed, 12/12)' },
|
|
956
|
+
stateHash: { type: 'string', description: 'Optional post-action state hash (from detect_noop)' },
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
}),
|
|
960
|
+
readOnlyTool({
|
|
961
|
+
name: 'get_action_receipts',
|
|
962
|
+
title: 'Get Action Receipts',
|
|
963
|
+
description: 'Read outcome-paired action receipts. Returns the receipt for a specific actionId, or the most recent receipts when no actionId is given.',
|
|
964
|
+
inputSchema: {
|
|
965
|
+
type: 'object',
|
|
966
|
+
properties: {
|
|
967
|
+
actionId: { type: 'string', description: 'Optional action id to fetch the matching receipt for' },
|
|
968
|
+
limit: { type: 'number', description: 'Max number of recent receipts to return when no actionId is given (default 20)' },
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
}),
|
|
838
972
|
readOnlyTool({
|
|
839
973
|
name: 'verify_claim',
|
|
840
974
|
description: 'Check whether a claim has enough tracked evidence before the agent asserts it.',
|
|
@@ -843,6 +977,7 @@ const TOOLS = [
|
|
|
843
977
|
required: ['claim'],
|
|
844
978
|
properties: {
|
|
845
979
|
claim: { type: 'string', description: 'The claim text to verify' },
|
|
980
|
+
goalContract: GOAL_CONTRACT_SCHEMA,
|
|
846
981
|
},
|
|
847
982
|
},
|
|
848
983
|
}),
|
|
@@ -1291,6 +1426,7 @@ const TOOLS = [
|
|
|
1291
1426
|
claim: { type: 'string', description: 'The completion claim text to verify (e.g. "Fix shipped", "Tests passing")' },
|
|
1292
1427
|
mode: { type: 'string', enum: ['blocking', 'advisory'], description: 'blocking (default) returns blocking=true when evidence missing; advisory returns blocking=false' },
|
|
1293
1428
|
sessionId: { type: 'string', description: 'Optional session id to associate with the gate decision' },
|
|
1429
|
+
goalContract: GOAL_CONTRACT_SCHEMA,
|
|
1294
1430
|
},
|
|
1295
1431
|
},
|
|
1296
1432
|
}),
|
|
@@ -1348,8 +1484,40 @@ const TOOLS = [
|
|
|
1348
1484
|
},
|
|
1349
1485
|
},
|
|
1350
1486
|
}),
|
|
1487
|
+
destructiveTool({
|
|
1488
|
+
name: 'parallel_workflow',
|
|
1489
|
+
description: 'Execute a parallel, multi-step subtask workflow to resolve an objective like a security audit, performance benchmark, or repository inspection.',
|
|
1490
|
+
inputSchema: {
|
|
1491
|
+
type: 'object',
|
|
1492
|
+
required: ['objective'],
|
|
1493
|
+
properties: {
|
|
1494
|
+
objective: { type: 'string', description: 'The objective to plan and execute (e.g. security audit, performance benchmark)' },
|
|
1495
|
+
concurrency: { type: 'number', description: 'Maximum parallel subtasks (default 3)' },
|
|
1496
|
+
timeoutMs: { type: 'number', description: 'Timeout in milliseconds (default 60000)' },
|
|
1497
|
+
},
|
|
1498
|
+
},
|
|
1499
|
+
}),
|
|
1351
1500
|
];
|
|
1352
1501
|
|
|
1502
|
+
// Normalize at export: guarantee EVERY tool carries a human-readable title and a
|
|
1503
|
+
// readOnlyHint/destructiveHint annotation (both required by the Connectors
|
|
1504
|
+
// Directory; the #1 rejection cause is missing annotations). Tools defined as
|
|
1505
|
+
// plain objects (not via readOnlyTool/destructiveTool) are backfilled here:
|
|
1506
|
+
// title from the name, and a conservative destructiveHint when no hint is set
|
|
1507
|
+
// (so an un-hinted tool is gated rather than silently treated as read-only).
|
|
1508
|
+
const NORMALIZED_TOOLS = TOOLS.map((tool) => {
|
|
1509
|
+
const title = tool.title || humanizeTitle(tool.name);
|
|
1510
|
+
const existing = tool.annotations || {};
|
|
1511
|
+
const hasHint = existing.readOnlyHint === true || existing.destructiveHint === true;
|
|
1512
|
+
const annotations = {
|
|
1513
|
+
title,
|
|
1514
|
+
...existing,
|
|
1515
|
+
...(hasHint ? {} : { destructiveHint: true }),
|
|
1516
|
+
};
|
|
1517
|
+
return { ...tool, title, annotations };
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1353
1520
|
module.exports = {
|
|
1354
|
-
TOOLS,
|
|
1521
|
+
TOOLS: NORMALIZED_TOOLS,
|
|
1522
|
+
humanizeTitle,
|
|
1355
1523
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Trajectory Scorer — Strategic Drift Detection.
|
|
6
|
+
*
|
|
7
|
+
* Measures the "Semantic Distance" between the original user intent
|
|
8
|
+
* (from primer.md) and the current set of changed files.
|
|
9
|
+
*
|
|
10
|
+
* If the agent modifies too many unrelated files, it triggers a safety block.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
function getTrajectoryScore(options = {}) {
|
|
18
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
19
|
+
const primerPath = path.join(projectRoot, 'primer.md');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(primerPath)) return { score: 0, isDrifting: false, drift: false };
|
|
22
|
+
|
|
23
|
+
const intent = fs.readFileSync(primerPath, 'utf8').toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Get currently modified files (unstaged + staged)
|
|
26
|
+
let changedFiles = options.changedFiles;
|
|
27
|
+
if (!changedFiles) {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync('git diff --name-only HEAD', { cwd: projectRoot, encoding: 'utf8' });
|
|
30
|
+
changedFiles = output.split('\n').filter(f => f.trim());
|
|
31
|
+
} catch {
|
|
32
|
+
return { score: 0, isDrifting: false, drift: false };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (changedFiles.length === 0) return { score: 0, isDrifting: false, drift: false };
|
|
37
|
+
|
|
38
|
+
// Calculate drift: How many changed files are NOT mentioned in the intent?
|
|
39
|
+
let driftCount = 0;
|
|
40
|
+
for (const file of changedFiles) {
|
|
41
|
+
const base = path.basename(file).toLowerCase();
|
|
42
|
+
if (!intent.includes(base)) {
|
|
43
|
+
driftCount++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const driftRatio = driftCount / changedFiles.length;
|
|
48
|
+
const isDrifting = driftRatio > 0.6 && changedFiles.length > 3;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
score: Number((1 - driftRatio).toFixed(2)),
|
|
52
|
+
changedCount: changedFiles.length,
|
|
53
|
+
driftCount,
|
|
54
|
+
isDrifting,
|
|
55
|
+
message: isDrifting
|
|
56
|
+
? `🚫 THUMBGATE: Strategic Drift Detected. You have modified ${changedFiles.length} files, but ${driftCount} of them were not mentioned in the original intent. Please refocus or update the intent.`
|
|
57
|
+
: null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
getTrajectoryScore
|
|
63
|
+
};
|
package/scripts/vector-store.js
CHANGED
|
@@ -172,6 +172,30 @@ async function embedWithGemini(text, options = {}) {
|
|
|
172
172
|
return values.map(Number);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
async function embedWithCoreAI(text, options = {}) {
|
|
176
|
+
if (process.platform !== 'darwin') {
|
|
177
|
+
throw new Error('Core AI is only supported on macOS');
|
|
178
|
+
}
|
|
179
|
+
const endpoint = process.env.THUMBGATE_COREAI_ENDPOINT || 'http://localhost:8088';
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch(`${endpoint}/embed`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({ text, options }),
|
|
185
|
+
signal: AbortSignal.timeout(2000),
|
|
186
|
+
});
|
|
187
|
+
if (res.ok) {
|
|
188
|
+
const payload = await res.json();
|
|
189
|
+
if (Array.isArray(payload.embedding)) {
|
|
190
|
+
return payload.embedding.map(Number);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new Error(`Core AI local service unavailable: ${err.message}`);
|
|
195
|
+
}
|
|
196
|
+
throw new Error('Core AI local service did not return a valid embedding');
|
|
197
|
+
}
|
|
198
|
+
|
|
175
199
|
async function embed(text, options = {}) {
|
|
176
200
|
if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') {
|
|
177
201
|
// Deterministic 384-dim unit vector: first element = 1.0, rest = 0.0
|
|
@@ -180,6 +204,26 @@ async function embed(text, options = {}) {
|
|
|
180
204
|
return stub;
|
|
181
205
|
}
|
|
182
206
|
const geminiConfig = resolveGeminiEmbeddingConfig();
|
|
207
|
+
if (geminiConfig.provider === 'coreai') {
|
|
208
|
+
try {
|
|
209
|
+
const vector = await embedWithCoreAI(text, options);
|
|
210
|
+
_lastEmbeddingProfile = {
|
|
211
|
+
generatedAt: new Date().toISOString(),
|
|
212
|
+
source: 'local-coreai',
|
|
213
|
+
activeProfile: {
|
|
214
|
+
id: 'coreai',
|
|
215
|
+
model: 'Core AI local model',
|
|
216
|
+
outputDimensionality: vector.length,
|
|
217
|
+
task: options.task || 'code retrieval',
|
|
218
|
+
rationale: 'Local Core AI Apple Silicon accelerated path.',
|
|
219
|
+
},
|
|
220
|
+
fallbackUsed: false,
|
|
221
|
+
};
|
|
222
|
+
return vector;
|
|
223
|
+
} catch (coreaiError) {
|
|
224
|
+
console.warn(`Core AI embedding failed, falling back to local: ${coreaiError.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
183
227
|
if (geminiConfig.enabled) {
|
|
184
228
|
try {
|
|
185
229
|
const vector = await embedWithGemini(text, options);
|
|
@@ -312,6 +356,7 @@ function setGeminiEmbedderForTests(loader) {
|
|
|
312
356
|
module.exports = {
|
|
313
357
|
upsertFeedback,
|
|
314
358
|
searchSimilar,
|
|
359
|
+
embed,
|
|
315
360
|
TABLE_NAME,
|
|
316
361
|
getEmbeddingConfig,
|
|
317
362
|
getLastEmbeddingProfile,
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* verify-marketing-pages-deployed.js — post-deploy probe for top-level
|
|
6
|
+
* marketing pages.
|
|
7
|
+
*
|
|
8
|
+
* Reads `config/post-deploy-marketing-pages.json` and curls every entry
|
|
9
|
+
* against the live production URL (default
|
|
10
|
+
* https://thumbgate-production.up.railway.app, overridable via
|
|
11
|
+
* THUMBGATE_PROD_URL env or --prod-url=…). Each response body must
|
|
12
|
+
* contain the configured sentinel string and must not contain any configured
|
|
13
|
+
* `mustNotContain` strings. Any mismatch fails the run and the route is
|
|
14
|
+
* included in the failure summary.
|
|
15
|
+
*
|
|
16
|
+
* Intended to run as a workflow step in .github/workflows/deploy-verify.yml
|
|
17
|
+
* after the version sentinel check, so the marketing surface is verified
|
|
18
|
+
* in the same gate as /health and /dashboard. Adding a new marketing
|
|
19
|
+
* page? Add it to the JSON manifest — no workflow edit required.
|
|
20
|
+
*
|
|
21
|
+
* Modes:
|
|
22
|
+
* --json machine-readable report on stdout, suitable for piping
|
|
23
|
+
* into the GitHub Actions PR comment step.
|
|
24
|
+
* --quiet suppress per-route lines; only print the final summary.
|
|
25
|
+
*
|
|
26
|
+
* Exit code is 0 when every page passes, 1 if any sentinel is missing
|
|
27
|
+
* or any route returns non-200.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
35
|
+
const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv = []) {
|
|
38
|
+
const out = {
|
|
39
|
+
prodUrl: process.env.THUMBGATE_PROD_URL || DEFAULT_PROD_URL,
|
|
40
|
+
manifestPath: DEFAULT_MANIFEST_PATH,
|
|
41
|
+
json: false,
|
|
42
|
+
quiet: false,
|
|
43
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
44
|
+
};
|
|
45
|
+
for (const arg of argv) {
|
|
46
|
+
if (arg === '--json') out.json = true;
|
|
47
|
+
else if (arg === '--quiet') out.quiet = true;
|
|
48
|
+
else if (arg.startsWith('--prod-url=')) out.prodUrl = arg.slice('--prod-url='.length);
|
|
49
|
+
else if (arg.startsWith('--manifest=')) out.manifestPath = arg.slice('--manifest='.length);
|
|
50
|
+
else if (arg.startsWith('--timeout-ms=')) {
|
|
51
|
+
const n = Number(arg.slice('--timeout-ms='.length));
|
|
52
|
+
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadManifest(manifestPath) {
|
|
59
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (!Array.isArray(parsed.pages) || parsed.pages.length === 0) {
|
|
62
|
+
throw new Error(`Manifest has no pages: ${manifestPath}`);
|
|
63
|
+
}
|
|
64
|
+
for (const entry of parsed.pages) {
|
|
65
|
+
if (typeof entry.route !== 'string' || !entry.route.startsWith('/')) {
|
|
66
|
+
throw new Error(`Invalid route in manifest: ${JSON.stringify(entry)}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof entry.sentinel !== 'string' || entry.sentinel.length === 0) {
|
|
69
|
+
throw new Error(`Invalid sentinel for route ${entry.route}`);
|
|
70
|
+
}
|
|
71
|
+
if (entry.mustNotContain != null) {
|
|
72
|
+
if (!Array.isArray(entry.mustNotContain) || entry.mustNotContain.some((value) => typeof value !== 'string' || value.length === 0)) {
|
|
73
|
+
throw new Error(`Invalid mustNotContain for route ${entry.route}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (entry.userAgent != null && (typeof entry.userAgent !== 'string' || entry.userAgent.length === 0)) {
|
|
77
|
+
throw new Error(`Invalid userAgent for route ${entry.route}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
84
|
+
if (typeof fetchImpl !== 'function') {
|
|
85
|
+
return { route, ok: false, error: 'fetch_unavailable' };
|
|
86
|
+
}
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
const url = `${prodUrl.replace(/\/$/, '')}${route}`;
|
|
91
|
+
const res = await fetchImpl(url, {
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
headers: {
|
|
94
|
+
// A real browser-shaped UA so bot-deflection interstitials do not
|
|
95
|
+
// trigger; this probe is meant to verify the buyer-facing surface.
|
|
96
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
|
97
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const body = await res.text().catch(() => '');
|
|
101
|
+
const sentinelPresent = body.includes(sentinel);
|
|
102
|
+
const forbiddenPresent = Array.isArray(mustNotContain)
|
|
103
|
+
? mustNotContain.filter((value) => body.includes(value))
|
|
104
|
+
: [];
|
|
105
|
+
return {
|
|
106
|
+
route,
|
|
107
|
+
url,
|
|
108
|
+
status: res.status,
|
|
109
|
+
ok: res.ok && sentinelPresent && forbiddenPresent.length === 0,
|
|
110
|
+
sentinelPresent,
|
|
111
|
+
forbiddenPresent,
|
|
112
|
+
bytes: body.length,
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
route,
|
|
117
|
+
ok: false,
|
|
118
|
+
error: error?.name === 'AbortError' ? `timeout_after_${timeoutMs}ms` : (error?.message || String(error)),
|
|
119
|
+
};
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
126
|
+
const manifest = loadManifest(manifestPath);
|
|
127
|
+
const results = [];
|
|
128
|
+
// Sequential is fine — manifest has <20 entries; parallelism would
|
|
129
|
+
// be over-engineered and would risk masking a per-route rate-limit signal.
|
|
130
|
+
for (const entry of manifest.pages) {
|
|
131
|
+
// eslint-disable-next-line no-await-in-loop
|
|
132
|
+
const result = await probePage({
|
|
133
|
+
prodUrl,
|
|
134
|
+
route: entry.route,
|
|
135
|
+
sentinel: entry.sentinel,
|
|
136
|
+
mustNotContain: entry.mustNotContain,
|
|
137
|
+
userAgent: entry.userAgent,
|
|
138
|
+
fetchImpl,
|
|
139
|
+
timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
|
|
142
|
+
}
|
|
143
|
+
const passed = results.filter((r) => r.ok);
|
|
144
|
+
const failed = results.filter((r) => !r.ok);
|
|
145
|
+
return {
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
prodUrl,
|
|
148
|
+
manifestVersion: manifest.version,
|
|
149
|
+
totalRoutes: results.length,
|
|
150
|
+
passedCount: passed.length,
|
|
151
|
+
failedCount: failed.length,
|
|
152
|
+
verdict: failed.length === 0 ? 'pass' : 'fail',
|
|
153
|
+
results,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderHuman(report, { quiet = false } = {}) {
|
|
158
|
+
const lines = [];
|
|
159
|
+
if (!quiet) {
|
|
160
|
+
for (const r of report.results) {
|
|
161
|
+
if (r.ok) {
|
|
162
|
+
lines.push(`✅ ${r.route.padEnd(20)} HTTP ${r.status} bytes=${r.bytes} sentinel-OK`);
|
|
163
|
+
} else if (r.error) {
|
|
164
|
+
lines.push(`❌ ${r.route.padEnd(20)} ERROR ${r.error}`);
|
|
165
|
+
} else if (!r.sentinelPresent) {
|
|
166
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} sentinel MISSING (expected: ${JSON.stringify(r.sentinel)})`);
|
|
167
|
+
} else if (Array.isArray(r.forbiddenPresent) && r.forbiddenPresent.length > 0) {
|
|
168
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} forbidden content PRESENT (${r.forbiddenPresent.map((value) => JSON.stringify(value)).join(', ')})`);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
lines.push(`\nSummary: ${report.passedCount}/${report.totalRoutes} pages passed against ${report.prodUrl} (verdict: ${report.verdict.toUpperCase()})`);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main(argv) {
|
|
179
|
+
const args = parseArgs(argv);
|
|
180
|
+
let report;
|
|
181
|
+
try {
|
|
182
|
+
report = await runVerification({
|
|
183
|
+
prodUrl: args.prodUrl,
|
|
184
|
+
manifestPath: args.manifestPath,
|
|
185
|
+
timeoutMs: args.timeoutMs,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
|
|
189
|
+
process.exit(2);
|
|
190
|
+
}
|
|
191
|
+
if (args.json) {
|
|
192
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stdout.write(`${renderHuman(report, { quiet: args.quiet })}\n`);
|
|
195
|
+
}
|
|
196
|
+
process.exitCode = report.verdict === 'pass' ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
DEFAULT_PROD_URL,
|
|
201
|
+
DEFAULT_TIMEOUT_MS,
|
|
202
|
+
DEFAULT_MANIFEST_PATH,
|
|
203
|
+
parseArgs,
|
|
204
|
+
loadManifest,
|
|
205
|
+
probePage,
|
|
206
|
+
runVerification,
|
|
207
|
+
renderHuman,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
211
|
+
main(process.argv.slice(2));
|
|
212
|
+
}
|