pandora-cli-skills 1.1.62 → 1.1.63

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/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pandora-cli-skills
3
3
  summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
4
- version: 1.1.62
4
+ version: 1.1.63
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -1923,7 +1923,7 @@ const commandContracts = [
1923
1923
  name: 'mirror.deploy',
1924
1924
  summary: 'Deploy a mirror market from selector or plan in dry-run or execute mode.',
1925
1925
  usage:
1926
- 'pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--oracle <address>] [--factory <address>] [--usdc <address>] [--distribution-yes <parts>] [--distribution-no <parts>] [--sources <url...>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--min-close-lead-seconds <n>]',
1926
+ 'pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--oracle <address>] [--factory <address>] [--usdc <address>] [--distribution-yes <parts>] [--distribution-no <parts>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--min-close-lead-seconds <n>]',
1927
1927
  emits: ['mirror.deploy', 'mirror.deploy.help'],
1928
1928
  dataSchema: '#/definitions/MirrorDeployPayload',
1929
1929
  mcpExposed: true,
@@ -1933,7 +1933,8 @@ const commandContracts = [
1933
1933
  executeRequiresValidation: true,
1934
1934
  notes: [
1935
1935
  'Mirror deploy dry-run returns the exact Pandora deployment payload and required validation ticket.',
1936
- 'Run agent.market.validate on that final payload before rerunning mirror.deploy with execute mode.',
1936
+ 'Mirror deploy never auto-copies Polymarket URLs into sources; pass independent public resolution URLs with --sources.',
1937
+ 'Run agent.market.validate on that final payload before rerunning mirror.deploy with execute mode, then pass --validation-ticket locally or agentPreflight in MCP.',
1937
1938
  ],
1938
1939
  },
1939
1940
  mcp: {
@@ -1961,6 +1962,7 @@ const commandContracts = [
1961
1962
  'distribution-yes': numberSchema('Initial YES distribution parts.', { minimum: 0 }),
1962
1963
  'distribution-no': numberSchema('Initial NO distribution parts.', { minimum: 0 }),
1963
1964
  sources: flexibleArraySchema(stringSchema(), 'Source URL list.'),
1965
+ 'validation-ticket': stringSchema('Ticket returned by agent.market.validate for the exact final payload (CLI execute mode).'),
1964
1966
  'manifest-file': stringSchema('Mirror manifest path.'),
1965
1967
  'polymarket-host': stringSchema('Polymarket host override.'),
1966
1968
  'polymarket-gamma-url': stringSchema('Polymarket Gamma API base URL.'),
@@ -2089,7 +2091,7 @@ const commandContracts = [
2089
2091
  name: 'mirror.go',
2090
2092
  summary: 'Run mirror deploy, verify, and optional sync workflow.',
2091
2093
  usage:
2092
- 'pandora [--output table|json] mirror go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--max-open-exposure-usdc <amount>] [--max-trades-per-day <n>] [--polymarket-rpc-url <url>] [--manifest-file <path>] [--dotenv-path <path>] [--rpc-url <url>] [--private-key <hex>]',
2094
+ 'pandora [--output table|json] mirror go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--max-open-exposure-usdc <amount>] [--max-trades-per-day <n>] [--polymarket-rpc-url <url>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--dotenv-path <path>] [--rpc-url <url>] [--private-key <hex>]',
2093
2095
  emits: ['mirror.go', 'mirror.go.help'],
2094
2096
  dataSchema: '#/definitions/MirrorDeployPayload',
2095
2097
  mcpExposed: true,
@@ -2099,6 +2101,7 @@ const commandContracts = [
2099
2101
  executeRequiresValidation: true,
2100
2102
  notes: [
2101
2103
  'Mirror go inherits the exact market payload from its deploy stage; use the returned validation ticket from paper/dry-run output.',
2104
+ 'When mirror go will execute a fresh deploy, provide independent public --sources and a matching validation ticket.',
2102
2105
  'Run agent.market.validate on that exact payload before rerunning mirror.go with execute or execute-live.',
2103
2106
  ],
2104
2107
  },
@@ -2125,6 +2128,8 @@ const commandContracts = [
2125
2128
  'max-open-exposure-usdc': numberSchema('Maximum open exposure in USDC.', { minimum: 0 }),
2126
2129
  'max-trades-per-day': integerSchema('Maximum daily trade count.', { minimum: 0 }),
2127
2130
  'polymarket-rpc-url': stringSchema('Polygon RPC URL for Polymarket preflight.'),
2131
+ sources: flexibleArraySchema(stringSchema(), 'Independent public source URL list.'),
2132
+ 'validation-ticket': stringSchema('Ticket returned by agent.market.validate for the exact final payload (CLI execute mode).'),
2128
2133
  'manifest-file': stringSchema('Mirror manifest path.'),
2129
2134
  'dotenv-path': stringSchema('Env file path.'),
2130
2135
  'rpc-url': commonFlags.rpcUrl,
@@ -44,8 +44,14 @@ function buildPolymarketPreflightCommand(cliName) {
44
44
  return `${cliName} polymarket preflight`;
45
45
  }
46
46
 
47
- function buildMirrorDeployRetryCommand(cliName) {
48
- return `${cliName} mirror deploy --dry-run --plan-file <plan-file>`;
47
+ function buildMirrorDeployRetryCommand(cliName, details) {
48
+ const selectorPlan = cleanToken(details && details.planFile, '')
49
+ ? `--plan-file ${cleanToken(details && details.planFile, '<plan-file>')}`
50
+ : '--plan-file <plan-file>';
51
+ const includeSourcesPlaceholder =
52
+ details
53
+ && (details.requiredMinimum || details.invalidSources || details.dependentSources || details.requiredValidation);
54
+ return `${cliName} mirror deploy --dry-run ${selectorPlan}${includeSourcesPlaceholder ? ' --sources <url1> <url2>' : ''}`;
49
55
  }
50
56
 
51
57
  function buildMirrorVerifyRetryCommand(cliName, details) {
@@ -232,12 +238,27 @@ function createErrorRecoveryService(options = {}) {
232
238
  command: buildPolymarketPreflightCommand(cliName),
233
239
  retryable: true,
234
240
  };
241
+ case 'MIRROR_VALIDATION_REQUIRED':
242
+ case 'MIRROR_VALIDATION_MISMATCH':
243
+ return {
244
+ action: 'Run market validation for the exact mirror payload and reuse the returned ticket',
245
+ command: buildAgentMarketValidateCommand(cliName, details),
246
+ retryable: true,
247
+ };
248
+ case 'MIRROR_RULES_FORMAT_INVALID':
249
+ case 'MIRROR_SOURCES_REQUIRED':
250
+ case 'MIRROR_SOURCES_INVALID':
251
+ return {
252
+ action: 'Rebuild mirror deploy inputs in dry-run mode and provide explicit independent sources',
253
+ command: buildMirrorDeployRetryCommand(cliName, details),
254
+ retryable: true,
255
+ };
235
256
  case 'MIRROR_DEPLOY_FAILED':
236
257
  case 'MIRROR_GO_FAILED':
237
258
  case 'MIRROR_GO_PREFLIGHT_FAILED':
238
259
  return {
239
260
  action: 'Re-run mirror deploy/verify in dry-run mode',
240
- command: buildMirrorDeployRetryCommand(cliName),
261
+ command: buildMirrorDeployRetryCommand(cliName, details),
241
262
  retryable: true,
242
263
  };
243
264
  case 'MIRROR_GO_VERIFY_FAILED':
@@ -225,6 +225,7 @@ function toToolDescriptor(definition) {
225
225
  canonicalTool: definition.canonicalTool || definition.aliasOf || definition.name,
226
226
  aliasOf: definition.aliasOf || null,
227
227
  preferred: definition.preferred !== false,
228
+ compatibilityAlias: Boolean(definition.aliasOf),
228
229
  mutating: Boolean(definition.mutating),
229
230
  longRunningBlocked: Boolean(definition.longRunningBlocked),
230
231
  controlInputNames: Array.isArray(definition.controlInputNames) ? [...definition.controlInputNames] : [],
@@ -238,7 +239,9 @@ function toToolDescriptor(definition) {
238
239
 
239
240
  return {
240
241
  name: definition.name,
241
- description: definition.description,
242
+ description: definition.aliasOf
243
+ ? `${definition.description} Compatibility alias for ${xPandora.canonicalTool}; prefer ${xPandora.canonicalTool}.`
244
+ : definition.description,
242
245
  inputSchema: {
243
246
  ...inputSchema,
244
247
  xPandora,
@@ -4,7 +4,7 @@
4
4
  * @type {string}
5
5
  */
6
6
  const MIRROR_GO_USAGE =
7
- 'pandora [--output table|json] mirror go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--chain-id <id>] [--rpc-url <url>] [--polymarket-rpc-url <url>] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--oracle <address>] [--factory <address>] [--sources <url...>] [--manifest-file <path>] [--trust-deploy] [--skip-gate] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--with-rules] [--include-similarity] [--min-close-lead-seconds <n>]';
7
+ 'pandora [--output table|json] mirror go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--chain-id <id>] [--rpc-url <url>] [--polymarket-rpc-url <url>] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--oracle <address>] [--factory <address>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--trust-deploy] [--skip-gate] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--with-rules] [--include-similarity] [--min-close-lead-seconds <n>]';
8
8
 
9
9
  /**
10
10
  * Canonical usage string for `mirror sync`.
@@ -97,7 +97,7 @@ function createRunMirrorCommand(deps) {
97
97
  ' plan --source polymarket --polymarket-market-id <id>|--polymarket-slug <slug> [--chain-id <id>] [--target-slippage-bps <n>] [--turnover-target <n>] [--depth-slippage-bps <n>] [--safety-multiplier <n>] [--min-liquidity-usdc <n>] [--max-liquidity-usdc <n>] [--with-rules] [--include-similarity] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
98
98
  );
99
99
  console.log(
100
- ' deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--oracle <address>] [--factory <address>] [--usdc <address>] [--distribution-yes <parts>] [--distribution-no <parts>] [--sources <url...>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--min-close-lead-seconds <n>]',
100
+ ' deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--oracle <address>] [--factory <address>] [--usdc <address>] [--distribution-yes <parts>] [--distribution-no <parts>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--min-close-lead-seconds <n>]',
101
101
  );
102
102
  console.log(
103
103
  ' verify --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> [--trust-deploy] [--manifest-file <path>] [--include-similarity] [--with-rules] [--allow-rule-mismatch] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
@@ -24,12 +24,12 @@ module.exports = async function handleMirrorDeploy({ shared, context, deps }) {
24
24
  context.outputMode,
25
25
  'mirror.deploy.help',
26
26
  commandHelpPayload(
27
- 'pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--manifest-file <path>] [--min-close-lead-seconds <n>]',
27
+ 'pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--min-close-lead-seconds <n>]',
28
28
  ),
29
29
  );
30
30
  } else {
31
31
  console.log(
32
- 'Usage: pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--manifest-file <path>] [--min-close-lead-seconds <n>]',
32
+ 'Usage: pandora [--output table|json] mirror deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--sources <url...>] [--validation-ticket <ticket>] [--manifest-file <path>] [--min-close-lead-seconds <n>]',
33
33
  );
34
34
  }
35
35
  return;
@@ -10,8 +10,10 @@ const {
10
10
  hashRules,
11
11
  preloadPandoraMatchCandidates,
12
12
  } = require('./mirror_verify_service.cjs');
13
+ const { buildRequiredAgentMarketValidation } = require('./agent_market_prompt_service.cjs');
13
14
  const { deployPandoraAmmMarket } = require('./pandora_deploy_service.cjs');
14
15
  const { defaultManifestFile, upsertPair } = require('./mirror_manifest_store.cjs');
16
+ const { isMcpMode } = require('./shared/mcp_path_guard.cjs');
15
17
  const { round } = require('./shared/utils.cjs');
16
18
 
17
19
  const MIRROR_PLAN_SCHEMA_VERSION = '1.0.0';
@@ -38,19 +40,310 @@ function normalizeSources(value) {
38
40
  .filter(Boolean);
39
41
  }
40
42
 
43
+ function normalizeComparableText(value) {
44
+ return String(value || '')
45
+ .toLowerCase()
46
+ .replace(/&/g, ' and ')
47
+ .replace(/[^a-z0-9]+/g, ' ')
48
+ .replace(/\b(the|fc|cf|sc|ac|club)\b/g, ' ')
49
+ .replace(/\s+/g, ' ')
50
+ .trim();
51
+ }
52
+
53
+ function hasPandoraBinaryRules(value) {
54
+ const text = String(value || '');
55
+ return /(^|\n)\s*YES\s*:/i.test(text) && /(^|\n)\s*NO\s*:/i.test(text);
56
+ }
57
+
58
+ function sanitizeParticipantLabel(value) {
59
+ return String(value || '')
60
+ .replace(/^["'`(\[]+/, '')
61
+ .replace(/["'`)\].,:;!?]+$/, '')
62
+ .trim();
63
+ }
64
+
65
+ function extractMatchParticipants(question) {
66
+ const text = String(question || '').trim();
67
+ if (!text) return [];
68
+
69
+ const patterns = [
70
+ /\bwill\s+(.+?)\s+(?:beat|defeat|top|topple|upset|outscore)\s+(.+?)(?:\?|$)/i,
71
+ /^(.+?)\s+(?:vs\.?|v\.?|@|at)\s+(.+?)(?:\?|$)/i,
72
+ /\bbetween\s+(.+?)\s+and\s+(.+?)(?:\?|$)/i,
73
+ ];
74
+
75
+ for (const pattern of patterns) {
76
+ const match = text.match(pattern);
77
+ if (!match) continue;
78
+ const left = sanitizeParticipantLabel(match[1]);
79
+ const right = sanitizeParticipantLabel(match[2]);
80
+ if (left && right) {
81
+ return Array.from(new Set([left, right]));
82
+ }
83
+ }
84
+
85
+ return [];
86
+ }
87
+
88
+ function extractResolveToSelection(description) {
89
+ const text = String(description || '').trim();
90
+ if (!text) return null;
91
+
92
+ const patterns = [
93
+ /\b(?:this market )?resolve(?:s|d)?\s+to\s+([^.;\n]+)/i,
94
+ /\bresolve(?:s|d)?\s+(?:in favor of|for)\s+([^.;\n]+)/i,
95
+ /\bwinner(?:\s+is)?\s*:\s*([^.;\n]+)/i,
96
+ ];
97
+
98
+ for (const pattern of patterns) {
99
+ const match = text.match(pattern);
100
+ if (match && match[1]) {
101
+ const candidate = sanitizeParticipantLabel(match[1]);
102
+ if (candidate) return candidate;
103
+ }
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ function findBestParticipantMatch(selection, participants) {
110
+ const normalizedSelection = normalizeComparableText(selection);
111
+ if (!normalizedSelection || !Array.isArray(participants) || !participants.length) {
112
+ return null;
113
+ }
114
+
115
+ let best = null;
116
+ let bestScore = 0;
117
+ const selectionTokens = new Set(normalizedSelection.split(' ').filter(Boolean));
118
+ for (const participant of participants) {
119
+ const normalizedParticipant = normalizeComparableText(participant);
120
+ if (!normalizedParticipant) continue;
121
+ if (
122
+ normalizedSelection === normalizedParticipant ||
123
+ normalizedSelection.includes(normalizedParticipant) ||
124
+ normalizedParticipant.includes(normalizedSelection)
125
+ ) {
126
+ return participant;
127
+ }
128
+ const participantTokens = new Set(normalizedParticipant.split(' ').filter(Boolean));
129
+ const overlap = [...selectionTokens].filter((token) => participantTokens.has(token)).length;
130
+ const score = overlap / Math.max(selectionTokens.size, participantTokens.size, 1);
131
+ if (score > bestScore) {
132
+ best = participant;
133
+ bestScore = score;
134
+ }
135
+ }
136
+
137
+ return bestScore >= 0.5 ? best : null;
138
+ }
139
+
140
+ function buildWinnerRules(selection, opponent) {
141
+ const yesBranch = `YES: The official winner of the event described in the market question is ${selection}.`;
142
+ const noBranch = opponent
143
+ ? `NO: The official winner is ${opponent}, or the event ends in a draw if an official draw is possible.`
144
+ : `NO: ${selection} is not the official winner of the event described in the market question.`;
145
+ const edgeBranch =
146
+ 'EDGE: If the event is canceled, postponed, abandoned, or no official result is declared by targetTimestamp, resolve NO.';
147
+ return [yesBranch, noBranch, edgeBranch].join('\n');
148
+ }
149
+
150
+ function buildQuestionFallbackRules(question) {
151
+ const normalizedQuestion = String(question || '').trim().replace(/\?+$/, '');
152
+ if (!normalizedQuestion) {
153
+ return 'YES: The market question resolves true by targetTimestamp.\nNO: The market question does not resolve true by targetTimestamp.\nEDGE: If the event is canceled, postponed, abandoned, or no official result is declared by targetTimestamp, resolve NO.';
154
+ }
155
+
156
+ const affirmative = normalizedQuestion.replace(/^will\s+/i, '').trim();
157
+ return [
158
+ `YES: ${affirmative.charAt(0).toUpperCase()}${affirmative.slice(1)}.`,
159
+ `NO: It is not true that ${affirmative}.`,
160
+ 'EDGE: If the event is canceled, postponed, abandoned, or no official result is declared by targetTimestamp, resolve NO.',
161
+ ].join('\n');
162
+ }
163
+
164
+ function assertPandoraBinaryRules(rulesText, details = {}) {
165
+ if (hasPandoraBinaryRules(rulesText)) {
166
+ return;
167
+ }
168
+ throw createServiceError(
169
+ 'MIRROR_RULES_FORMAT_INVALID',
170
+ 'Mirror rules must use explicit Pandora YES:/NO: branches before deploy. Re-run mirror plan --with-rules or pass --rules with binary Pandora rules.',
171
+ details,
172
+ );
173
+ }
174
+
175
+ function isPolymarketSourceUrl(value) {
176
+ try {
177
+ const parsed = new URL(String(value || '').trim());
178
+ return /(^|\.)polymarket\.com$/i.test(parsed.hostname);
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ function assertIndependentMirrorSources(sources) {
185
+ const normalized = normalizeSources(sources);
186
+ const invalidSources = [];
187
+ const dependentSources = [];
188
+ const distinctSources = new Set();
189
+ const distinctHosts = new Set();
190
+
191
+ for (const source of normalized) {
192
+ try {
193
+ const parsed = new URL(source);
194
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
195
+ invalidSources.push(source);
196
+ continue;
197
+ }
198
+ if (isPolymarketSourceUrl(source)) {
199
+ dependentSources.push(source);
200
+ continue;
201
+ }
202
+ distinctSources.add(parsed.toString());
203
+ distinctHosts.add(parsed.hostname.toLowerCase());
204
+ } catch {
205
+ invalidSources.push(source);
206
+ }
207
+ }
208
+
209
+ if (!normalized.length) {
210
+ throw createServiceError(
211
+ 'MIRROR_SOURCES_REQUIRED',
212
+ 'Mirror deploy requires explicit independent resolution sources via --sources. Polymarket URLs are never used automatically.',
213
+ {
214
+ requiredMinimum: 2,
215
+ normalizedCount: 0,
216
+ },
217
+ );
218
+ }
219
+
220
+ if (invalidSources.length) {
221
+ throw createServiceError(
222
+ 'MIRROR_SOURCES_INVALID',
223
+ '--sources must contain valid http(s) URLs.',
224
+ {
225
+ invalidSources,
226
+ },
227
+ );
228
+ }
229
+
230
+ if (dependentSources.length) {
231
+ throw createServiceError(
232
+ 'MIRROR_SOURCES_INVALID',
233
+ 'Mirror deploy requires independent resolution sources. Polymarket URLs are not allowed in --sources.',
234
+ {
235
+ dependentSources,
236
+ },
237
+ );
238
+ }
239
+
240
+ if (normalized.length < 2) {
241
+ throw createServiceError(
242
+ 'MIRROR_SOURCES_REQUIRED',
243
+ 'Mirror deploy requires at least two independent resolution sources in --sources.',
244
+ {
245
+ requiredMinimum: 2,
246
+ normalizedCount: normalized.length,
247
+ },
248
+ );
249
+ }
250
+
251
+ if (distinctSources.size < 2 || distinctHosts.size < 2) {
252
+ throw createServiceError(
253
+ 'MIRROR_SOURCES_REQUIRED',
254
+ 'Mirror deploy requires at least two independent resolution sources from different hosts in --sources.',
255
+ {
256
+ requiredMinimum: 2,
257
+ normalizedCount: normalized.length,
258
+ distinctSourceCount: distinctSources.size,
259
+ distinctHostCount: distinctHosts.size,
260
+ },
261
+ );
262
+ }
263
+
264
+ return normalized;
265
+ }
266
+
267
+ function assertMirrorValidationTicket({ execute, question, rules, sources, targetTimestamp, validationTicket }) {
268
+ const requiredValidation = buildRequiredAgentMarketValidation({
269
+ question,
270
+ rules,
271
+ sources,
272
+ targetTimestamp,
273
+ });
274
+
275
+ if (!execute || isMcpMode()) {
276
+ return {
277
+ requiredValidation,
278
+ agentValidation: null,
279
+ };
280
+ }
281
+
282
+ const providedTicket = String(validationTicket || '').trim();
283
+ if (!providedTicket) {
284
+ throw createServiceError(
285
+ 'MIRROR_VALIDATION_REQUIRED',
286
+ 'mirror execute requires --validation-ticket from agent market validate for the exact final mirror payload.',
287
+ {
288
+ requiredValidation,
289
+ },
290
+ );
291
+ }
292
+
293
+ if (providedTicket !== requiredValidation.ticket) {
294
+ throw createServiceError(
295
+ 'MIRROR_VALIDATION_MISMATCH',
296
+ 'Provided --validation-ticket does not match the exact final mirror market payload.',
297
+ {
298
+ expectedTicket: requiredValidation.ticket,
299
+ receivedTicket: providedTicket,
300
+ requiredValidation,
301
+ },
302
+ );
303
+ }
304
+
305
+ return {
306
+ requiredValidation,
307
+ agentValidation: {
308
+ ok: true,
309
+ ticket: providedTicket,
310
+ decision: 'PASS',
311
+ summary: 'Validated via CLI ticket gate.',
312
+ },
313
+ };
314
+ }
315
+
41
316
  function buildRuleTemplate(sourceMarket) {
42
317
  const diagnostics = [];
318
+ const sourceQuestion = String(sourceMarket && sourceMarket.question ? sourceMarket.question : '').trim();
43
319
  const sourceDescription = String(sourceMarket && sourceMarket.description ? sourceMarket.description : '').trim();
44
- if (sourceDescription) {
320
+ if (sourceDescription && hasPandoraBinaryRules(sourceDescription)) {
45
321
  return {
46
322
  rulesText: sourceDescription,
47
323
  diagnostics,
48
324
  };
49
325
  }
50
326
 
51
- diagnostics.push('Source market description/rules missing; generated fallback rule template.');
327
+ const selectedOutcome = extractResolveToSelection(sourceDescription);
328
+ if (selectedOutcome) {
329
+ const participants = extractMatchParticipants(sourceQuestion);
330
+ const selectedParticipant = findBestParticipantMatch(selectedOutcome, participants) || selectedOutcome;
331
+ const opposingParticipant =
332
+ participants.find((participant) => normalizeComparableText(participant) !== normalizeComparableText(selectedParticipant)) || null;
333
+
334
+ diagnostics.push('Translated source market resolution text into Pandora YES/NO rules.');
335
+ if (participants.length >= 2 && !opposingParticipant) {
336
+ diagnostics.push('Unable to confidently determine the opposing side; NO branch resolves when the selected side does not win.');
337
+ }
338
+ return {
339
+ rulesText: buildWinnerRules(selectedParticipant, opposingParticipant),
340
+ diagnostics,
341
+ };
342
+ }
343
+
344
+ diagnostics.push('Source rules were not already in Pandora YES/NO format; generated fallback binary rule template from the source question.');
52
345
  return {
53
- rulesText: `Resolves YES if \"${String(sourceMarket && sourceMarket.question ? sourceMarket.question : 'the source condition').trim()}\" is true by the deadline. Resolves NO otherwise; canceled/postponed/abandoned/unresolved => NO.`,
346
+ rulesText: buildQuestionFallbackRules(sourceQuestion || 'the source condition'),
54
347
  diagnostics,
55
348
  };
56
349
  }
@@ -251,15 +544,31 @@ async function deployMirror(options = {}) {
251
544
  }
252
545
 
253
546
  const diagnostics = [];
254
- const sourceRulesText = String(
547
+ const question = String(planData.sourceMarket && planData.sourceMarket.question ? planData.sourceMarket.question : '').trim();
548
+ const targetTimestamp = Number(planData.sourceMarket && planData.sourceMarket.closeTimestamp);
549
+ const refreshedRuleTemplate = buildRuleTemplate({
550
+ ...(planData.sourceMarket || {}),
551
+ description:
552
+ (planData.rules && (planData.rules.sourceRules || planData.rules.proposedPandoraRules))
553
+ || (planData.sourceMarket && planData.sourceMarket.description)
554
+ || '',
555
+ });
556
+ let sourceRulesText = String(
255
557
  options.rules ||
256
558
  (planData.rules && (planData.rules.proposedPandoraRules || planData.rules.sourceRules)) ||
257
559
  (planData.sourceMarket && planData.sourceMarket.description) ||
258
560
  '',
259
561
  ).trim();
260
-
261
- const question = String(planData.sourceMarket && planData.sourceMarket.question ? planData.sourceMarket.question : '').trim();
262
- const targetTimestamp = Number(planData.sourceMarket && planData.sourceMarket.closeTimestamp);
562
+ if (!options.rules && !hasPandoraBinaryRules(sourceRulesText) && hasPandoraBinaryRules(refreshedRuleTemplate.rulesText)) {
563
+ sourceRulesText = refreshedRuleTemplate.rulesText;
564
+ diagnostics.push('Upgraded non-binary source rules to Pandora YES/NO format during deploy.');
565
+ }
566
+ assertPandoraBinaryRules(sourceRulesText, {
567
+ question,
568
+ sourceRules: planData.rules && planData.rules.sourceRules ? planData.rules.sourceRules : null,
569
+ suggestedRules: refreshedRuleTemplate.rulesText,
570
+ });
571
+ diagnostics.push(...refreshedRuleTemplate.diagnostics);
263
572
 
264
573
  const liquidityUsdc =
265
574
  options.liquidityUsdc !== null && options.liquidityUsdc !== undefined
@@ -275,20 +584,15 @@ async function deployMirror(options = {}) {
275
584
  ? Number(options.distributionNo)
276
585
  : Number(planData.distributionHint && planData.distributionHint.distributionNo);
277
586
 
278
- const sources = normalizeSources(options.sources);
279
- if (options.sourcesProvided && sources.length < 2) {
280
- throw createServiceError(
281
- 'INVALID_FLAG_VALUE',
282
- '--sources requires at least two non-empty URLs when explicitly provided.',
283
- {
284
- providedCount: Array.isArray(options.sources) ? options.sources.length : 0,
285
- normalizedCount: sources.length,
286
- },
287
- );
288
- }
289
- if (!options.sourcesProvided && sources.length < 2) {
290
- diagnostics.push('Using fallback source URLs because explicit --sources were not provided.');
291
- }
587
+ const sources = assertIndependentMirrorSources(options.sources);
588
+ const validationGate = assertMirrorValidationTicket({
589
+ execute: Boolean(options.execute),
590
+ question,
591
+ rules: sourceRulesText,
592
+ sources,
593
+ targetTimestamp,
594
+ validationTicket: options.validationTicket,
595
+ });
292
596
 
293
597
  let deployPayload;
294
598
  try {
@@ -302,7 +606,7 @@ async function deployMirror(options = {}) {
302
606
  usdc: options.usdc,
303
607
  question,
304
608
  rules: sourceRulesText,
305
- sources: sources.length >= 2 ? sources : ['https://polymarket.com', 'https://clob.polymarket.com'],
609
+ sources,
306
610
  targetTimestamp,
307
611
  minCloseLeadSeconds: Number.isFinite(Number(options.minCloseLeadSeconds))
308
612
  ? Number(options.minCloseLeadSeconds)
@@ -393,8 +697,8 @@ async function deployMirror(options = {}) {
393
697
  planDigest: planData.planDigest || buildPlanDigest(planData),
394
698
  deploymentArgs: deployPayload.deploymentArgs,
395
699
  dryRun: deployPayload.mode === 'dry-run',
396
- requiredValidation: deployPayload.requiredValidation || null,
397
- agentValidation: deployPayload.agentValidation || null,
700
+ requiredValidation: deployPayload.requiredValidation || validationGate.requiredValidation || null,
701
+ agentValidation: deployPayload.agentValidation || validationGate.agentValidation || null,
398
702
  tx: deployPayload.tx,
399
703
  pandora: deployPayload.pandora,
400
704
  postDeployChecks,
@@ -120,6 +120,7 @@ function createParseMirrorDeployFlags(deps) {
120
120
  distributionYesPct: null,
121
121
  distributionNoPct: null,
122
122
  rules: null,
123
+ validationTicket: null,
123
124
  polymarketHost: null,
124
125
  polymarketGammaUrl: null,
125
126
  polymarketGammaMockUrl: null,
@@ -251,6 +252,11 @@ function createParseMirrorDeployFlags(deps) {
251
252
  i += 1;
252
253
  continue;
253
254
  }
255
+ if (token === '--validation-ticket') {
256
+ options.validationTicket = requireFlagValue(args, i, '--validation-ticket');
257
+ i += 1;
258
+ continue;
259
+ }
254
260
  if (token === '--distribution-yes') {
255
261
  options.distributionYes = parseDistributionUnits(
256
262
  requireFlagValue(args, i, '--distribution-yes'),
@@ -133,6 +133,7 @@ function createParseMirrorGoFlags(deps) {
133
133
  distributionNoPct: null,
134
134
  sources: [],
135
135
  sourcesProvided: false,
136
+ validationTicket: null,
136
137
  manifestFile: null,
137
138
  trustDeploy: false,
138
139
  forceGate: false,
@@ -373,6 +374,11 @@ function createParseMirrorGoFlags(deps) {
373
374
  i = j - 1;
374
375
  continue;
375
376
  }
377
+ if (token === '--validation-ticket') {
378
+ options.validationTicket = requireFlagValue(args, i, '--validation-ticket');
379
+ i += 1;
380
+ continue;
381
+ }
376
382
  if (token === '--manifest-file') {
377
383
  options.manifestFile = requireFlagValue(args, i, '--manifest-file');
378
384
  i += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pandora-cli-skills",
3
- "version": "1.1.62",
3
+ "version": "1.1.63",
4
4
  "description": "Pandora CLI & Skills",
5
5
  "main": "cli/pandora.cjs",
6
6
  "bin": {