pandora-cli-skills 1.1.49 → 1.1.51

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.
@@ -367,7 +367,7 @@ Mirror advanced flags (for operator tuning):
367
367
 
368
368
  ### Resolve command
369
369
  - Usage:
370
- - `pandora [--output table|json] resolve --poll-address <address> --answer yes|no --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
370
+ - `pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
371
371
  - Behavior:
372
372
  - `--dry-run` returns a deterministic execution plan.
373
373
  - `--execute` submits the resolution transaction with decoded revert diagnostics on failure.
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.49
4
+ version: 1.1.51
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -145,7 +145,7 @@ pandora [--output table|json] webhook test [--webhook-url <url>] [--webhook-temp
145
145
  pandora [--output table|json] leaderboard [--metric profit|volume|win-rate] [--chain-id <id>] [--limit <n>] [--min-trades <n>]
146
146
  pandora [--output table|json] analyze --market-address <address> [--provider <name>] [--model <id>] [--max-cost-usd <n>] [--temperature <n>] [--timeout-ms <ms>]
147
147
  pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
148
- pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
148
+ pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
149
149
  pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
150
150
  pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
151
151
  pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
@@ -418,7 +418,7 @@ pandora --output json schema
418
418
 
419
419
  ### Resolve command
420
420
  - Usage:
421
- - `pandora [--output table|json] resolve --poll-address <address> --answer yes|no --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
421
+ - `pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
422
422
  - Behavior:
423
423
  - `--dry-run` returns the call plan and decode-ready payload.
424
424
  - `--execute` submits on-chain resolution through configured oracle/operator path.
@@ -35,6 +35,11 @@ const CONTRACT_ERROR_ABI = [
35
35
  },
36
36
  ];
37
37
 
38
+ const REVERT_SELECTOR_HINTS = {
39
+ // Market-specific minimum-notional guard seen on some Pandora AMM deployments.
40
+ '0x7e2d7787': 'Trade too small for this market. Increase --amount-usdc and retry.',
41
+ };
42
+
38
43
  function isHexData(value) {
39
44
  return /^0x[0-9a-fA-F]*$/.test(String(value || ''));
40
45
  }
@@ -126,6 +131,10 @@ function formatDecodedContractError(decoded) {
126
131
  return decoded.errorName;
127
132
  }
128
133
  if (decoded.data) {
134
+ const selector = String(decoded.data).slice(0, 10).toLowerCase();
135
+ if (REVERT_SELECTOR_HINTS[selector]) {
136
+ return `${REVERT_SELECTOR_HINTS[selector]} (selector ${selector})`;
137
+ }
129
138
  return `Contract reverted (${decoded.data})`;
130
139
  }
131
140
  return null;
@@ -102,82 +102,143 @@ async function runMirrorSync(options, deps = {}) {
102
102
  break;
103
103
  }
104
104
 
105
- resetDailyCountersIfNeeded(state, tickAt);
106
-
107
- const verifyPayload =
108
- iteration === 1 && startupVerifyPayload
109
- ? startupVerifyPayload
110
- : await verifyFn(buildVerifyRequest(options));
111
-
112
- const snapshotMetrics = evaluateSnapshot(verifyPayload, options);
113
- const plan = buildTickPlan({
114
- snapshotMetrics,
115
- state,
116
- options,
117
- });
118
- const depth = await fetchDepthSnapshot({
119
- depthFn,
120
- verifyPayload,
121
- options,
122
- });
123
-
124
- const gate = applyGateBypassPolicy(
125
- evaluateStrictGates(
126
- buildTickGateContext({
127
- verifyPayload,
105
+ try {
106
+ resetDailyCountersIfNeeded(state, tickAt);
107
+
108
+ const verifyPayload =
109
+ iteration === 1 && startupVerifyPayload
110
+ ? startupVerifyPayload
111
+ : await verifyFn(buildVerifyRequest(options));
112
+
113
+ const snapshotMetrics = evaluateSnapshot(verifyPayload, options);
114
+ const plan = buildTickPlan({
115
+ snapshotMetrics,
116
+ state,
117
+ options,
118
+ });
119
+ const depth = await fetchDepthSnapshot({
120
+ depthFn,
121
+ verifyPayload,
122
+ options,
123
+ });
124
+
125
+ const gate = applyGateBypassPolicy(
126
+ evaluateStrictGates(
127
+ buildTickGateContext({
128
+ verifyPayload,
129
+ options,
130
+ state,
131
+ plan,
132
+ snapshotMetrics,
133
+ depth,
134
+ minimumTimeToCloseSec,
135
+ }),
136
+ ),
137
+ options,
138
+ );
139
+
140
+ const snapshot = buildTickSnapshot({
141
+ iteration,
142
+ tickAt,
143
+ verifyPayload,
144
+ options,
145
+ snapshotMetrics,
146
+ plan,
147
+ depth,
148
+ gate,
149
+ });
150
+
151
+ if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
152
+ await processTriggeredAction({
128
153
  options,
129
154
  state,
155
+ snapshot,
130
156
  plan,
157
+ gate,
158
+ tickAt,
159
+ loadedFilePath: loaded.filePath,
160
+ rebalanceFn,
161
+ hedgeFn,
162
+ sendWebhook,
163
+ strategyHash: hash,
164
+ iteration,
165
+ actions,
166
+ webhookReports,
131
167
  snapshotMetrics,
168
+ verifyPayload,
132
169
  depth,
133
- minimumTimeToCloseSec,
134
- }),
135
- ),
136
- options,
137
- );
138
-
139
- const snapshot = buildTickSnapshot({
140
- iteration,
141
- tickAt,
142
- verifyPayload,
143
- options,
144
- snapshotMetrics,
145
- plan,
146
- depth,
147
- gate,
148
- });
149
-
150
- if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
151
- await processTriggeredAction({
152
- options,
170
+ });
171
+ }
172
+
173
+ await persistTickSnapshot({
174
+ loadedFilePath: loaded.filePath,
153
175
  state,
154
- snapshot,
155
- plan,
156
- gate,
157
176
  tickAt,
177
+ snapshot,
178
+ snapshots,
179
+ onTick,
180
+ iteration,
181
+ });
182
+ } catch (err) {
183
+ const errorCode = err && err.code ? String(err.code) : 'MIRROR_SYNC_TICK_FAILED';
184
+ const errorMessage = err && err.message ? err.message : String(err);
185
+ const errorDetails = err && err.details !== undefined ? err.details : null;
186
+ const timestamp = tickAt.toISOString();
187
+
188
+ const diagnostic = {
189
+ level: 'error',
190
+ scope: 'tick',
191
+ iteration,
192
+ timestamp,
193
+ code: errorCode,
194
+ message: errorMessage,
195
+ retryable: options.mode !== 'once',
196
+ };
197
+ if (errorDetails !== null) diagnostic.details = errorDetails;
198
+ diagnostics.push(diagnostic);
199
+
200
+ const snapshot = {
201
+ schemaVersion: MIRROR_SYNC_SCHEMA_VERSION,
202
+ timestamp,
203
+ iteration,
204
+ metrics: {
205
+ driftBps: null,
206
+ plannedRebalanceUsdc: 0,
207
+ plannedHedgeUsdc: 0,
208
+ },
209
+ strictGate: {
210
+ ok: false,
211
+ failedChecks: [],
212
+ checks: [],
213
+ },
214
+ action: {
215
+ status: 'error',
216
+ failedChecks: [],
217
+ forcedGateBypass: false,
218
+ errorCode,
219
+ errorMessage,
220
+ },
221
+ error: {
222
+ code: errorCode,
223
+ message: errorMessage,
224
+ details: errorDetails,
225
+ },
226
+ };
227
+
228
+ await persistTickSnapshot({
158
229
  loadedFilePath: loaded.filePath,
159
- rebalanceFn,
160
- hedgeFn,
161
- sendWebhook,
162
- strategyHash: hash,
230
+ state,
231
+ tickAt,
232
+ snapshot,
233
+ snapshots,
234
+ onTick,
163
235
  iteration,
164
- actions,
165
- webhookReports,
166
- snapshotMetrics,
167
- verifyPayload,
168
- depth,
169
236
  });
170
- }
171
237
 
172
- await persistTickSnapshot({
173
- loadedFilePath: loaded.filePath,
174
- state,
175
- tickAt,
176
- snapshot,
177
- snapshots,
178
- onTick,
179
- iteration,
180
- });
238
+ if (options.mode === 'once') {
239
+ throw err;
240
+ }
241
+ }
181
242
 
182
243
  if (shouldStop) break;
183
244
  if (iteration >= maxIterations) break;
@@ -7,6 +7,18 @@ function requireDep(deps, name) {
7
7
  return deps[name];
8
8
  }
9
9
 
10
+ function normalizeSources(entries) {
11
+ const values = [];
12
+ for (const entry of Array.isArray(entries) ? entries : []) {
13
+ const parts = String(entry || '').split(/[\n,]/g);
14
+ for (const part of parts) {
15
+ const normalized = String(part || '').trim();
16
+ if (normalized) values.push(normalized);
17
+ }
18
+ }
19
+ return values;
20
+ }
21
+
10
22
  /**
11
23
  * Creates the mirror deploy flags parser.
12
24
  * @param {object} deps
@@ -250,6 +262,12 @@ function createParseMirrorDeployFlags(deps) {
250
262
  ) {
251
263
  throw new CliError('INVALID_ARGS', '--distribution-yes + --distribution-no must equal 1000000000.');
252
264
  }
265
+ if (options.sourcesProvided && normalizeSources(options.sources).length < 2) {
266
+ throw new CliError(
267
+ 'INVALID_FLAG_VALUE',
268
+ '--sources requires at least two non-empty URLs when explicitly provided.',
269
+ );
270
+ }
253
271
 
254
272
  return options;
255
273
  };
@@ -7,6 +7,18 @@ function requireDep(deps, name) {
7
7
  return deps[name];
8
8
  }
9
9
 
10
+ function normalizeSources(entries) {
11
+ const values = [];
12
+ for (const entry of Array.isArray(entries) ? entries : []) {
13
+ const parts = String(entry || '').split(/[\n,]/g);
14
+ for (const part of parts) {
15
+ const normalized = String(part || '').trim();
16
+ if (normalized) values.push(normalized);
17
+ }
18
+ }
19
+ return values;
20
+ }
21
+
10
22
  /**
11
23
  * Creates the mirror go flags parser.
12
24
  * @param {object} deps
@@ -354,6 +366,12 @@ function createParseMirrorGoFlags(deps) {
354
366
  );
355
367
  }
356
368
  }
369
+ if (options.sourcesProvided && normalizeSources(options.sources).length < 2) {
370
+ throw new CliError(
371
+ 'INVALID_FLAG_VALUE',
372
+ '--sources requires at least two non-empty URLs when explicitly provided.',
373
+ );
374
+ }
357
375
 
358
376
  return options;
359
377
  };
@@ -14,6 +14,8 @@ function createRunResolveCommand(deps) {
14
14
  const includesHelpFlag = requireDep(deps, 'includesHelpFlag');
15
15
  const emitSuccess = requireDep(deps, 'emitSuccess');
16
16
  const commandHelpPayload = requireDep(deps, 'commandHelpPayload');
17
+ const parseIndexerSharedFlags = requireDep(deps, 'parseIndexerSharedFlags');
18
+ const maybeLoadTradeEnv = requireDep(deps, 'maybeLoadTradeEnv');
17
19
  const parseResolveFlags = requireDep(deps, 'parseResolveFlags');
18
20
  const runResolve = requireDep(deps, 'runResolve');
19
21
  const renderSingleEntityTable = requireDep(deps, 'renderSingleEntityTable');
@@ -21,24 +23,26 @@ function createRunResolveCommand(deps) {
21
23
  const assertLiveWriteAllowed = typeof deps.assertLiveWriteAllowed === 'function' ? deps.assertLiveWriteAllowed : null;
22
24
 
23
25
  return async function runResolveCommand(args, context) {
24
- if (includesHelpFlag(args)) {
26
+ const shared = parseIndexerSharedFlags(args);
27
+ if (includesHelpFlag(shared.rest)) {
25
28
  if (context.outputMode === 'json') {
26
29
  emitSuccess(
27
30
  context.outputMode,
28
31
  'resolve.help',
29
32
  commandHelpPayload(
30
- 'pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
33
+ 'pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
31
34
  ),
32
35
  );
33
36
  } else {
34
37
  // eslint-disable-next-line no-console
35
38
  console.log(
36
- 'Usage: pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
39
+ 'Usage: pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
37
40
  );
38
41
  }
39
42
  return;
40
43
  }
41
- const options = parseResolveFlags(args);
44
+ maybeLoadTradeEnv(shared);
45
+ const options = parseResolveFlags(shared.rest);
42
46
  if (options.execute && assertLiveWriteAllowed) {
43
47
  await assertLiveWriteAllowed('resolve.execute', {
44
48
  runtimeMode: options.fork || options.forkRpcUrl ? 'fork' : 'live',
package/cli/pandora.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { spawnSync } = require('child_process');
6
7
  const { createCommandRouter } = require('./lib/command_router.cjs');
@@ -513,7 +514,13 @@ function getDoctorServiceInstance() {
513
514
  }
514
515
 
515
516
  const ROOT = path.resolve(__dirname, '..');
516
- const DEFAULT_ENV_FILE = path.join(ROOT, 'scripts', '.env');
517
+ const DEFAULT_ENV_FILE_PRIMARY = path.join(ROOT, 'scripts', '.env');
518
+ const DEFAULT_ENV_FILE_FALLBACK = path.join(os.homedir(), '.pandora-cli.env');
519
+ const DEFAULT_ENV_FILE = fs.existsSync(DEFAULT_ENV_FILE_FALLBACK)
520
+ ? DEFAULT_ENV_FILE_FALLBACK
521
+ : fs.existsSync(DEFAULT_ENV_FILE_PRIMARY)
522
+ ? DEFAULT_ENV_FILE_PRIMARY
523
+ : DEFAULT_ENV_FILE_FALLBACK;
517
524
  const DEFAULT_ENV_EXAMPLE = path.join(ROOT, 'scripts', '.env.example');
518
525
  const DEFAULT_INDEXER_URL = SHARED_DEFAULT_INDEXER_URL;
519
526
  let PACKAGE_VERSION = '0.0.0';
@@ -793,7 +800,7 @@ Usage:
793
800
  pandora [--output table|json] leaderboard [--metric profit|volume|win-rate] [--chain-id <id>] [--limit <n>] [--min-trades <n>]
794
801
  pandora [--output table|json] analyze --market-address <address> [--provider <name>] [--model <id>] [--max-cost-usd <n>] [--temperature <n>] [--timeout-ms <ms>]
795
802
  pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
796
- pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
803
+ pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
797
804
  pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>|--all] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
798
805
  pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
799
806
  pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
@@ -852,7 +859,7 @@ Examples:
852
859
 
853
860
  Notes:
854
861
  - launch/clone-bet forward unknown flags directly to underlying scripts.
855
- - scripts/.env is loaded automatically for launch/clone-bet unless --skip-dotenv is used.
862
+ - Env auto-load default: ~/.pandora-cli.env when present; otherwise scripts/.env. Use --skip-dotenv to disable.
856
863
  - --output json is supported for all commands except launch/clone-bet.
857
864
  - Indexer URL resolution order: --indexer-url, PANDORA_INDEXER_URL, INDEXER_URL, default public indexer.
858
865
  - mirror status --with-live can enrich output with Polymarket position data when POLYMARKET_* credentials are set; missing endpoints/creds return diagnostics instead of hard failures.
@@ -4746,6 +4753,8 @@ const runResolveCommandFromService = createRunResolveCommand({
4746
4753
  includesHelpFlag,
4747
4754
  emitSuccess,
4748
4755
  commandHelpPayload,
4756
+ parseIndexerSharedFlags,
4757
+ maybeLoadTradeEnv,
4749
4758
  parseResolveFlags: parseResolveFlagsFromModule,
4750
4759
  runResolve,
4751
4760
  renderSingleEntityTable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pandora-cli-skills",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "description": "Pandora CLI & Skills",
5
5
  "main": "cli/pandora.cjs",
6
6
  "bin": {
@@ -68,6 +68,7 @@
68
68
  "dependencies": {
69
69
  "@modelcontextprotocol/sdk": "^1.27.1",
70
70
  "@polymarket/clob-client": "^5.2.4",
71
+ "playwright-core": "^1.58.2",
71
72
  "tsx": "^4.21.0",
72
73
  "viem": "^2.46.2",
73
74
  "ws": "^8.19.0"
@@ -1,6 +1,7 @@
1
1
  const test = require('node:test');
2
2
  const assert = require('node:assert/strict');
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
 
6
7
  const {
@@ -5612,6 +5613,28 @@ test('resolve and lp commands are enabled', () => {
5612
5613
  assert.equal(lpPayload.data.wallet, ADDRESSES.wallet1.toLowerCase());
5613
5614
  });
5614
5615
 
5616
+ test('resolve accepts --dotenv-path and returns env-file errors instead of unknown-flag', () => {
5617
+ const missingFile = path.join(os.tmpdir(), `pandora-missing-env-${Date.now()}.env`);
5618
+ const result = runCli([
5619
+ '--output',
5620
+ 'json',
5621
+ 'resolve',
5622
+ '--dotenv-path',
5623
+ missingFile,
5624
+ '--poll-address',
5625
+ '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
5626
+ '--answer',
5627
+ 'yes',
5628
+ '--reason',
5629
+ 'fixture',
5630
+ '--dry-run',
5631
+ ]);
5632
+
5633
+ assert.equal(result.status, 1);
5634
+ const payload = parseJsonOutput(result);
5635
+ assert.equal(payload.error.code, 'ENV_FILE_NOT_FOUND');
5636
+ });
5637
+
5615
5638
  test('launch enforces mode flag and dry-run reaches deterministic preflight', () => {
5616
5639
  const args = buildLaunchArgs();
5617
5640
 
@@ -43,6 +43,7 @@ const {
43
43
  runPolymarketPreflight,
44
44
  POLYMARKET_OPS_SCHEMA_VERSION,
45
45
  } = require('../../cli/lib/polymarket_ops_service.cjs');
46
+ const { formatDecodedContractError } = require('../../cli/lib/contract_error_decoder.cjs');
46
47
  const { runMirrorSync } = require('../../cli/lib/mirror_sync_service.cjs');
47
48
  const { createRunMirrorCommand } = require('../../cli/lib/mirror_command_service.cjs');
48
49
  const { resolveForkRuntime } = require('../../cli/lib/fork_runtime_service.cjs');
@@ -260,6 +261,12 @@ test('evaluateMarket throws deterministic error without provider', async () => {
260
261
  );
261
262
  });
262
263
 
264
+ test('contract error formatter maps known minimum-trade selector to actionable hint', () => {
265
+ const message = formatDecodedContractError({ data: '0x7e2d7787' });
266
+ assert.match(message, /trade too small/i);
267
+ assert.match(message, /--amount-usdc/i);
268
+ });
269
+
263
270
  test('autopilot state helpers are deterministic', () => {
264
271
  const hash1 = strategyHash({ a: 1, b: 'x' });
265
272
  const hash2 = strategyHash({ a: 1, b: 'x' });
@@ -1486,6 +1493,165 @@ test('runMirrorSync handles thrown hedgeFn errors without consuming idempotency'
1486
1493
  }
1487
1494
  });
1488
1495
 
1496
+ test('runMirrorSync run mode continues after transient tick verification failures', async () => {
1497
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-tick-retry-'));
1498
+ const stateFile = path.join(tempDir, 'mirror-state.json');
1499
+ const killSwitchFile = path.join(tempDir, 'STOP');
1500
+
1501
+ const verifyPayload = {
1502
+ matchConfidence: 0.99,
1503
+ gateResult: {
1504
+ ok: true,
1505
+ failedChecks: [],
1506
+ checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
1507
+ },
1508
+ sourceMarket: {
1509
+ source: 'polymarket',
1510
+ marketId: 'poly-cond-1',
1511
+ yesPct: 60,
1512
+ yesTokenId: 'yes-token',
1513
+ noTokenId: 'no-token',
1514
+ },
1515
+ pandora: {
1516
+ yesPct: 55,
1517
+ reserveYes: 5,
1518
+ reserveNo: 5,
1519
+ },
1520
+ expiry: { minTimeToExpirySec: 7200 },
1521
+ };
1522
+
1523
+ let verifyCallCount = 0;
1524
+
1525
+ try {
1526
+ const payload = await runMirrorSync(
1527
+ {
1528
+ mode: 'run',
1529
+ iterations: 3,
1530
+ indexerUrl: 'https://example.invalid/graphql',
1531
+ timeoutMs: 1000,
1532
+ pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
1533
+ polymarketMarketId: 'poly-cond-1',
1534
+ executeLive: false,
1535
+ trustDeploy: false,
1536
+ hedgeEnabled: false,
1537
+ hedgeRatio: 1,
1538
+ intervalMs: 1,
1539
+ driftTriggerBps: 10000,
1540
+ hedgeTriggerUsdc: 1000,
1541
+ maxRebalanceUsdc: 25,
1542
+ maxHedgeUsdc: 10,
1543
+ maxOpenExposureUsdc: 100,
1544
+ maxTradesPerDay: 10,
1545
+ cooldownMs: 1000,
1546
+ depthSlippageBps: 100,
1547
+ stateFile,
1548
+ killSwitchFile,
1549
+ polymarketHost: 'https://clob.polymarket.com',
1550
+ },
1551
+ {
1552
+ verifyFn: async () => {
1553
+ verifyCallCount += 1;
1554
+ if (verifyCallCount === 2) {
1555
+ const error = new Error('temporary indexer timeout');
1556
+ error.code = 'INDEXER_TIMEOUT';
1557
+ throw error;
1558
+ }
1559
+ return verifyPayload;
1560
+ },
1561
+ depthFn: async () => ({
1562
+ depthWithinSlippageUsd: 1000,
1563
+ yesDepth: { depthUsd: 1000, midPrice: 0.4, worstPrice: 0.41 },
1564
+ noDepth: { depthUsd: 1000, midPrice: 0.6, worstPrice: 0.61 },
1565
+ }),
1566
+ sleep: async () => {},
1567
+ },
1568
+ );
1569
+
1570
+ assert.equal(payload.iterationsCompleted, 3);
1571
+ assert.equal(payload.snapshots.length, 3);
1572
+ assert.equal(payload.diagnostics.length, 1);
1573
+ assert.equal(payload.diagnostics[0].code, 'INDEXER_TIMEOUT');
1574
+ assert.equal(payload.diagnostics[0].scope, 'tick');
1575
+ assert.equal(payload.snapshots[1].action.status, 'error');
1576
+ assert.equal(payload.snapshots[1].error.code, 'INDEXER_TIMEOUT');
1577
+ } finally {
1578
+ fs.rmSync(tempDir, { recursive: true, force: true });
1579
+ }
1580
+ });
1581
+
1582
+ test('runMirrorSync once mode still fails fast on tick errors', async () => {
1583
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-once-fail-'));
1584
+ const stateFile = path.join(tempDir, 'mirror-state.json');
1585
+ const killSwitchFile = path.join(tempDir, 'STOP');
1586
+
1587
+ const verifyPayload = {
1588
+ matchConfidence: 0.99,
1589
+ gateResult: {
1590
+ ok: true,
1591
+ failedChecks: [],
1592
+ checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
1593
+ },
1594
+ sourceMarket: {
1595
+ source: 'polymarket',
1596
+ marketId: 'poly-cond-1',
1597
+ yesPct: 60,
1598
+ yesTokenId: 'yes-token',
1599
+ noTokenId: 'no-token',
1600
+ },
1601
+ pandora: {
1602
+ yesPct: 55,
1603
+ reserveYes: 5,
1604
+ reserveNo: 5,
1605
+ },
1606
+ expiry: { minTimeToExpirySec: 7200 },
1607
+ };
1608
+
1609
+ try {
1610
+ await assert.rejects(
1611
+ runMirrorSync(
1612
+ {
1613
+ mode: 'once',
1614
+ indexerUrl: 'https://example.invalid/graphql',
1615
+ timeoutMs: 1000,
1616
+ pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
1617
+ polymarketMarketId: 'poly-cond-1',
1618
+ executeLive: false,
1619
+ trustDeploy: false,
1620
+ hedgeEnabled: false,
1621
+ hedgeRatio: 1,
1622
+ intervalMs: 1,
1623
+ driftTriggerBps: 10000,
1624
+ hedgeTriggerUsdc: 1000,
1625
+ maxRebalanceUsdc: 25,
1626
+ maxHedgeUsdc: 10,
1627
+ maxOpenExposureUsdc: 100,
1628
+ maxTradesPerDay: 10,
1629
+ cooldownMs: 1000,
1630
+ depthSlippageBps: 100,
1631
+ stateFile,
1632
+ killSwitchFile,
1633
+ polymarketHost: 'https://clob.polymarket.com',
1634
+ },
1635
+ {
1636
+ verifyFn: async () => verifyPayload,
1637
+ depthFn: async () => {
1638
+ const error = new Error('depth fetch unavailable');
1639
+ error.code = 'DEPTH_FETCH_FAILED';
1640
+ throw error;
1641
+ },
1642
+ sleep: async () => {},
1643
+ },
1644
+ ),
1645
+ (error) => {
1646
+ assert.equal(error.code, 'DEPTH_FETCH_FAILED');
1647
+ return true;
1648
+ },
1649
+ );
1650
+ } finally {
1651
+ fs.rmSync(tempDir, { recursive: true, force: true });
1652
+ }
1653
+ });
1654
+
1489
1655
  test('runAutopilot does not consume budget/idempotency when executeFn throws', async () => {
1490
1656
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-autopilot-failure-'));
1491
1657
  const stateFile = path.join(tempDir, 'autopilot-state.json');
@@ -1732,6 +1898,41 @@ test('createParseMirrorDeployFlags enforces secure --rpc-url and drops dead allo
1732
1898
  );
1733
1899
  });
1734
1900
 
1901
+ test('createParseMirrorDeployFlags rejects explicit empty or underspecified --sources early', () => {
1902
+ const parseMirrorDeployFlags = createParseMirrorDeployFlags(buildParserDeps());
1903
+
1904
+ assert.throws(
1905
+ () =>
1906
+ parseMirrorDeployFlags([
1907
+ '--plan-file',
1908
+ '/tmp/plan.json',
1909
+ '--dry-run',
1910
+ '--sources',
1911
+ '',
1912
+ ]),
1913
+ (error) => {
1914
+ assert.equal(error.code, 'INVALID_FLAG_VALUE');
1915
+ assert.match(error.message, /at least two non-empty urls/i);
1916
+ return true;
1917
+ },
1918
+ );
1919
+
1920
+ assert.throws(
1921
+ () =>
1922
+ parseMirrorDeployFlags([
1923
+ '--plan-file',
1924
+ '/tmp/plan.json',
1925
+ '--dry-run',
1926
+ '--sources',
1927
+ 'https://example.com/one',
1928
+ ]),
1929
+ (error) => {
1930
+ assert.equal(error.code, 'INVALID_FLAG_VALUE');
1931
+ return true;
1932
+ },
1933
+ );
1934
+ });
1935
+
1735
1936
  test('mirror sync gates normalize and selectively bypass failed checks', () => {
1736
1937
  assert.deepEqual(
1737
1938
  normalizeSkipGateChecks([' depth_coverage ', 'unknown_check', 'MAX_TRADES_PER_DAY', 'DEPTH_COVERAGE']),
@@ -1884,6 +2085,39 @@ test('createParseMirrorGoFlags treats bare --skip-gate as force gate mode', () =
1884
2085
  assert.deepEqual(options.skipGateChecks, []);
1885
2086
  });
1886
2087
 
2088
+ test('createParseMirrorGoFlags rejects explicit empty or underspecified --sources early', () => {
2089
+ const parseMirrorGoFlags = createParseMirrorGoFlags(buildParserDeps());
2090
+
2091
+ assert.throws(
2092
+ () =>
2093
+ parseMirrorGoFlags([
2094
+ '--polymarket-market-id',
2095
+ 'poly-1',
2096
+ '--sources',
2097
+ '',
2098
+ ]),
2099
+ (error) => {
2100
+ assert.equal(error.code, 'INVALID_FLAG_VALUE');
2101
+ assert.match(error.message, /at least two non-empty urls/i);
2102
+ return true;
2103
+ },
2104
+ );
2105
+
2106
+ assert.throws(
2107
+ () =>
2108
+ parseMirrorGoFlags([
2109
+ '--polymarket-market-id',
2110
+ 'poly-1',
2111
+ '--sources',
2112
+ 'https://example.com/one',
2113
+ ]),
2114
+ (error) => {
2115
+ assert.equal(error.code, 'INVALID_FLAG_VALUE');
2116
+ return true;
2117
+ },
2118
+ );
2119
+ });
2120
+
1887
2121
  test('createParseLifecycleFlags validates start|status|resolve contracts', () => {
1888
2122
  const parseLifecycleFlags = createParseLifecycleFlags({
1889
2123
  CliError: ParserCliError,