pi-oracle 0.7.11 → 0.7.13

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.
@@ -248,9 +248,29 @@ function hasLegacyEffortCombobox(entries) {
248
248
  });
249
249
  }
250
250
 
251
- function compactSelectionFromEntry(entry, _entries, _options = {}) {
252
- if (entry.disabled || !COMPACT_INTELLIGENCE_CONTROL_KINDS.has(entry.kind || "")) return undefined;
253
- return parseCompactIntelligenceSelection(entry.label);
251
+ function compactSelectionFromEntry(entry, _entries, options = {}) {
252
+ if (entry.disabled) return undefined;
253
+ const kind = entry.kind || "";
254
+ if (COMPACT_INTELLIGENCE_CONTROL_KINDS.has(kind)) return parseCompactIntelligenceSelection(entry.label);
255
+ if (options.allowClosedButtons && kind === "button" && !/\bexpanded=true\b/.test(String(entry.line || ""))) {
256
+ return parseCompactIntelligenceSelection(entry.label);
257
+ }
258
+ return undefined;
259
+ }
260
+
261
+ export function matchesCompactIntelligenceControlLabel(label) {
262
+ return Boolean(parseCompactIntelligenceSelection(label));
263
+ }
264
+
265
+ export function snapshotHasClosedCompactSelection(snapshot, selection) {
266
+ /** @type {SnapshotEntry[]} */
267
+ const entries = parseSnapshotEntries(snapshot);
268
+ if (hasRemovableComposerModelChip(entries) || hasLegacyEffortCombobox(entries) || hasCompactIntelligenceMenuContext(entries)) return false;
269
+ return entries.some((entry) => {
270
+ if (entry.kind !== "button" || entry.disabled) return false;
271
+ const compactSelection = compactSelectionFromEntry(entry, entries, { allowClosedButtons: true });
272
+ return compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection);
273
+ });
254
274
  }
255
275
 
256
276
  function compactSelectionMatchesRequested(selection, compactSelection) {
@@ -23,12 +23,14 @@ import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABE
23
23
  import {
24
24
  buildAllowedChatGptOrigins,
25
25
  deriveAssistantCompletionSignature,
26
+ matchesCompactIntelligenceControlLabel,
26
27
  matchesCompactIntelligenceOpenerLabel,
27
28
  matchesModelFamilyLabel,
28
29
  matchesRequestedModelControlLabel,
29
30
  requestedEffortLabel,
30
31
  effortSelectionVisible,
31
32
  snapshotCanSafelySkipModelConfiguration,
33
+ snapshotHasClosedCompactSelection,
32
34
  snapshotHasModelConfigurationUi,
33
35
  snapshotHasModelOpener,
34
36
  snapshotHasUsableComposerControls,
@@ -78,6 +80,7 @@ const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
78
80
  const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
79
81
  const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
80
82
  const PROFILE_CLONE_TIMEOUT_MS = 120_000;
83
+ const MODEL_CONFIGURATION_OPEN_TIMEOUT_MS = 45_000;
81
84
  const MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS = 20_000;
82
85
  const MODEL_CONFIGURATION_SETTLE_POLL_MS = 250;
83
86
  const MODEL_CONFIGURATION_CLOSE_RETRY_MS = 1_000;
@@ -1091,15 +1094,9 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
1091
1094
  return { state: "challenge_blocking", message: "ChatGPT is showing a challenge/verification page" };
1092
1095
  }
1093
1096
 
1094
- const outagePatterns = [
1095
- /something went wrong/i,
1096
- /a network error occurred/i,
1097
- /an error occurred while connecting to the websocket/i,
1098
- /try again later/i,
1099
- /rate limit/i,
1100
- ];
1101
- if (outagePatterns.some((pattern) => pattern.test(text))) {
1102
- return { state: "transient_outage_error", message: "ChatGPT is showing a transient outage/error page" };
1097
+ const outageText = detectProviderTransientErrorText(text);
1098
+ if (outageText) {
1099
+ return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/rate-limit page: ${outageText}` };
1103
1100
  }
1104
1101
 
1105
1102
  const allowedOrigins = buildAllowedChatGptOrigins(job.config.browser.chatUrl, job.config.browser.authUrl);
@@ -1162,8 +1159,9 @@ function classifyGrokPage({ url, snapshot, body }) {
1162
1159
  if (/captcha|cloudflare|verify you are human|unusual activity|suspicious activity/i.test(text)) {
1163
1160
  return { state: "challenge_blocking", message: "Grok is showing a challenge/verification page" };
1164
1161
  }
1165
- if (/something went wrong|network error|try again later|rate limit/i.test(text)) {
1166
- return { state: "transient_outage_error", message: "Grok is showing a transient outage/error page" };
1162
+ const outageText = detectProviderTransientErrorText(text);
1163
+ if (outageText) {
1164
+ return { state: "transient_outage_error", message: `Grok is showing a transient outage/rate-limit page: ${outageText}` };
1167
1165
  }
1168
1166
  const onGrokOrigin = typeof url === "string" && url.startsWith("https://grok.com");
1169
1167
  if (onGrokOrigin && hasGrokLoginCta(text)) {
@@ -1250,6 +1248,42 @@ function detectUploadErrorText(text) {
1250
1248
  return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
1251
1249
  }
1252
1250
 
1251
+ function detectProviderTransientErrorText(text) {
1252
+ const patterns = [
1253
+ "Too many requests",
1254
+ "rate limit",
1255
+ "try again later",
1256
+ "Something went wrong",
1257
+ "A network error occurred",
1258
+ "An error occurred while connecting to the websocket",
1259
+ ];
1260
+ return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
1261
+ }
1262
+
1263
+ function detectProviderVisibleBlockerText(text) {
1264
+ const patterns = [
1265
+ "Too many requests",
1266
+ "rate limit",
1267
+ ];
1268
+ return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
1269
+ }
1270
+
1271
+ function formatProviderTransientErrorMessage(job, errorText, context) {
1272
+ const providerLabel = isGrokJob(job) ? "Grok" : "ChatGPT";
1273
+ return `${providerLabel} is showing a transient outage/rate-limit page${context ? ` while ${context}` : ""}: ${errorText}`;
1274
+ }
1275
+
1276
+ function providerTransientErrorMessage(job, text, context) {
1277
+ const errorText = detectProviderVisibleBlockerText(text);
1278
+ if (!errorText) return "";
1279
+ return formatProviderTransientErrorMessage(job, errorText, context);
1280
+ }
1281
+
1282
+ function throwIfProviderTransientError(job, text, context) {
1283
+ const message = providerTransientErrorMessage(job, text, context);
1284
+ if (message) throw new Error(message);
1285
+ }
1286
+
1253
1287
  function detectResponseFailureText(text) {
1254
1288
  const patterns = [
1255
1289
  "Message delivery timed out",
@@ -1289,6 +1323,7 @@ async function waitForUploadConfirmed(job, fileLabel, baselineCount) {
1289
1323
  while (Date.now() < timeoutAt) {
1290
1324
  await heartbeat();
1291
1325
  const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
1326
+ throwIfProviderTransientError(job, snapshot, "uploading the archive");
1292
1327
 
1293
1328
  const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
1294
1329
  if (errorText) {
@@ -1323,6 +1358,7 @@ async function waitForSendReady(job) {
1323
1358
  await heartbeat();
1324
1359
  const snapshot = await snapshotText(job);
1325
1360
  const body = await pageText(job).catch(() => "");
1361
+ throwIfProviderTransientError(job, snapshot, "waiting for send readiness");
1326
1362
  const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
1327
1363
  if (errorText) {
1328
1364
  throw new Error(`Upload error detected: ${errorText}`);
@@ -1366,6 +1402,7 @@ async function sendAcceptanceState(job, baselineAssistantCount) {
1366
1402
  urlKnown: urlResult.ok,
1367
1403
  assistantCount: Math.max(baselineAssistantCount, messages.length),
1368
1404
  stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
1405
+ transientErrorText: detectProviderVisibleBlockerText(snapshot) || "",
1369
1406
  };
1370
1407
  }
1371
1408
 
@@ -1386,6 +1423,7 @@ async function waitForSendAccepted(job, beforeSend, options = {}) {
1386
1423
  while (Date.now() < timeoutAt) {
1387
1424
  await heartbeat();
1388
1425
  const afterSend = await sendAcceptanceState(job, beforeSend.assistantCount || 0);
1426
+ if (afterSend.transientErrorText) throw new Error(formatProviderTransientErrorMessage(job, afterSend.transientErrorText, "waiting for send acceptance"));
1389
1427
  if (providerSendAccepted(beforeSend, afterSend)) return true;
1390
1428
  await sleep(500);
1391
1429
  }
@@ -1420,12 +1458,13 @@ async function dismissProFeedbackModal(job, snapshot) {
1420
1458
  }
1421
1459
 
1422
1460
  async function openModelConfiguration(job) {
1423
- const timeoutAt = Date.now() + 15_000;
1461
+ const timeoutAt = Date.now() + MODEL_CONFIGURATION_OPEN_TIMEOUT_MS;
1424
1462
  let lastSnapshot = "";
1425
1463
 
1426
1464
  while (Date.now() < timeoutAt) {
1427
1465
  const initialSnapshot = await snapshotText(job);
1428
1466
  lastSnapshot = initialSnapshot;
1467
+ throwIfProviderTransientError(job, initialSnapshot, "opening model configuration");
1429
1468
  if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
1430
1469
  if (await dismissProFeedbackModal(job, initialSnapshot)) continue;
1431
1470
 
@@ -1438,6 +1477,7 @@ async function openModelConfiguration(job) {
1438
1477
  await agentBrowser(job, "wait", "800");
1439
1478
  const after = await snapshotText(job);
1440
1479
  lastSnapshot = after;
1480
+ throwIfProviderTransientError(job, after, "opening model configuration");
1441
1481
  if (snapshotHasModelConfigurationUi(after)) return after;
1442
1482
  if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
1443
1483
 
@@ -1451,6 +1491,7 @@ async function openModelConfiguration(job) {
1451
1491
  await agentBrowser(job, "wait", "1200");
1452
1492
  const postConfigure = await snapshotText(job);
1453
1493
  lastSnapshot = postConfigure;
1494
+ throwIfProviderTransientError(job, postConfigure, "opening model configuration");
1454
1495
  if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
1455
1496
  if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
1456
1497
  }
@@ -1544,22 +1585,28 @@ async function configureModel(job) {
1544
1585
  throw new Error(`Could not find model family control for ${job.selection.modelFamily}`);
1545
1586
  }
1546
1587
 
1588
+ let compactSelectionVerifiedAfterClick = false;
1547
1589
  if (!alreadyConfiguredInUi && !familyAlreadySelectedInUi && familyEntry) {
1590
+ const clickedCompactControl = matchesCompactIntelligenceControlLabel(familyEntry.label);
1548
1591
  await clickRef(job, familyEntry.ref);
1549
1592
  await agentBrowser(job, "wait", "800");
1550
1593
  familySnapshot = await snapshotText(job);
1551
1594
  verificationSnapshot = familySnapshot;
1595
+ compactSelectionVerifiedAfterClick = clickedCompactControl && snapshotHasClosedCompactSelection(familySnapshot, job.selection);
1596
+ if (compactSelectionVerifiedAfterClick) {
1597
+ await log(`Verified compact ChatGPT selection after menu close for family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}`);
1598
+ }
1552
1599
  const postClickControlOptions = {
1553
1600
  ignoreCompactTierButtons: snapshotHasCompactIntelligenceMenuControls(familySnapshot),
1554
1601
  ignoreCompactOnlyButtons: snapshotHasLegacyEffortCombobox(familySnapshot),
1555
1602
  };
1556
1603
  familyEntry = findEntry(familySnapshot, (candidate) => matchesRequestedModelControl(candidate, job.selection, postClickControlOptions));
1557
- if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
1604
+ if (!compactSelectionVerifiedAfterClick && !familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
1558
1605
  throw new Error(`Requested model family did not remain selected: ${job.selection.modelFamily}`);
1559
1606
  }
1560
1607
  }
1561
1608
 
1562
- if (job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") {
1609
+ if ((job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") && !compactSelectionVerifiedAfterClick) {
1563
1610
  const effortLabel = requestedEffortLabel(job.selection);
1564
1611
  if (effortLabel && !effortSelectionVisible(familySnapshot, effortLabel)) {
1565
1612
  const opened = await openEffortDropdown(job);
@@ -1589,7 +1636,8 @@ async function configureModel(job) {
1589
1636
  if (job.selection.modelFamily === "instant") {
1590
1637
  const desiredAutoSwitchState = job.selection.autoSwitchToThinking === true;
1591
1638
  const currentAutoSwitchState = autoSwitchToThinkingSelectionVisible(familySnapshot);
1592
- const compactInstantAlreadyVerified = desiredAutoSwitchState && currentAutoSwitchState === undefined && snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection);
1639
+ const compactInstantAlreadyVerified = compactSelectionVerifiedAfterClick
1640
+ || (desiredAutoSwitchState && currentAutoSwitchState === undefined && snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection));
1593
1641
  if (!compactInstantAlreadyVerified && currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
1594
1642
  await clickAutoSwitchToThinkingControl(job);
1595
1643
  await agentBrowser(job, "wait", "400");
@@ -1598,7 +1646,7 @@ async function configureModel(job) {
1598
1646
  }
1599
1647
  }
1600
1648
 
1601
- const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
1649
+ const stronglyVerified = compactSelectionVerifiedAfterClick || snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
1602
1650
  if (!stronglyVerified) {
1603
1651
  throw new Error(`Could not verify requested model settings in configuration UI for ${job.selection.modelFamily}`);
1604
1652
  }
@@ -1793,6 +1841,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
1793
1841
  const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming");
1794
1842
  const hasRetryButton = snapshot.includes('button "Retry"');
1795
1843
  const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
1844
+ throwIfProviderTransientError(job, snapshot, "waiting for response completion");
1796
1845
  const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
1797
1846
  const messages = await assistantMessages(job);
1798
1847
  const targetMessage = messages[baselineAssistantCount];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  "platform-smoke.config.mjs",
37
37
  "scripts/platform-smoke.mjs",
38
38
  "scripts/platform-smoke",
39
- "scripts/oracle-real-smoke.mjs"
39
+ "scripts/oracle-real-smoke.mjs",
40
+ "scripts/oracle-chatgpt-preset-proof.mjs"
40
41
  ],
41
42
  "pi": {
42
43
  "extensions": [
@@ -49,7 +50,7 @@
49
50
  "typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
50
51
  "sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
51
52
  "pack:check": "npm pack --dry-run",
52
- "verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
53
+ "verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
53
54
  "test": "npm run verify:oracle",
54
55
  "prepublishOnly": "npm run release:check",
55
56
  "check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/targets.mjs && node scripts/platform-smoke/invariants.mjs",
@@ -61,12 +62,14 @@
61
62
  "smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
62
63
  "smoke:real": "npm run smoke:real:packed",
63
64
  "smoke:real:doctor": "node scripts/oracle-real-smoke.mjs doctor",
64
- "release:check": "npm run verify:oracle && npm run smoke:platform:all",
65
+ "release:check": "npm run verify:oracle && npm run release:proof:chatgpt-presets && npm run smoke:platform:all",
65
66
  "check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs",
67
+ "check:oracle-release-proof": "node --check scripts/oracle-chatgpt-preset-proof.mjs",
68
+ "release:proof:chatgpt-presets": "node scripts/oracle-chatgpt-preset-proof.mjs check",
66
69
  "smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
67
70
  "smoke:real:source": "node scripts/oracle-real-smoke.mjs run --mode source",
68
71
  "sanity:oracle:platform": "node scripts/oracle-sanity-runner.mjs --mode platform",
69
- "verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run sanity:oracle:platform && npm run pack:check"
72
+ "verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run sanity:oracle:platform && npm run pack:check"
70
73
  },
71
74
  "dependencies": {
72
75
  "@steipete/sweet-cookie": "^0.3.0"
@@ -23,7 +23,7 @@ export default {
23
23
  commands: ["npm run smoke:platform:all"],
24
24
  },
25
25
  release: {
26
- description: "Full release gate: local verification plus the doctor-first platform matrix.",
26
+ description: "Full release gate: local verification, fresh ChatGPT preset proof, plus the doctor-first platform matrix.",
27
27
  commands: ["npm run release:check"],
28
28
  },
29
29
  },
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ // Purpose: Release-blocking proof gate for live ChatGPT preset selection.
3
+ // Responsibilities: Validate that a fresh manual/live oracle job matrix covered every canonical ChatGPT preset before publish.
4
+ // Scope: Maintainer release safety only; the script does not submit jobs or touch provider accounts.
5
+ // Usage: npm run release:proof:chatgpt-presets, or `node scripts/oracle-chatgpt-preset-proof.mjs template`.
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = resolve(SCRIPT_DIR, "..");
14
+ const DEFAULT_PROOF_PATH = ".artifacts/chatgpt-preset-proof/latest.json";
15
+ const PROOF_PATH_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF";
16
+ const JOBS_DIR_ENV = "PI_ORACLE_JOBS_DIR";
17
+ const MAX_AGE_HOURS_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF_MAX_AGE_HOURS";
18
+ const DEFAULT_MAX_AGE_HOURS = 72;
19
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
+ const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
21
+
22
+ function usage() {
23
+ console.log(`Usage: node scripts/oracle-chatgpt-preset-proof.mjs <check|template>
24
+
25
+ Commands:
26
+ check Validate release-blocking live ChatGPT preset proof. Default.
27
+ template Print a non-valid proof-file template for the current package version/git head.
28
+
29
+ Environment:
30
+ ${PROOF_PATH_ENV} Proof JSON path (default: ${DEFAULT_PROOF_PATH})
31
+ ${JOBS_DIR_ENV} Oracle jobs root for job lookup (default also checks /tmp)
32
+ ${MAX_AGE_HOURS_ENV} Freshness window in hours (default: ${DEFAULT_MAX_AGE_HOURS})
33
+
34
+ Proof file contract:
35
+ The proof must reference live oracle job state produced by the loaded extension
36
+ after the current git HEAD. It must include one completed ChatGPT job per
37
+ canonical ORACLE_SUBMIT_PRESETS id. Shape-only proof is rejected.
38
+ `);
39
+ }
40
+
41
+ function fail(message) {
42
+ console.error(message);
43
+ process.exitCode = 1;
44
+ }
45
+
46
+ function readJson(path) {
47
+ return JSON.parse(readFileSync(path, "utf8"));
48
+ }
49
+
50
+ function git(args) {
51
+ return execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf8" }).trim();
52
+ }
53
+
54
+ function packageMetadata() {
55
+ const pkg = readJson(resolve(REPO_ROOT, "package.json"));
56
+ return { name: pkg.name, version: pkg.version };
57
+ }
58
+
59
+ function currentGitHead() {
60
+ return git(["rev-parse", "HEAD"]);
61
+ }
62
+
63
+ function currentGitHeadCommittedAt() {
64
+ return git(["show", "-s", "--format=%cI", "HEAD"]);
65
+ }
66
+
67
+ function currentGitStatus() {
68
+ return git(["status", "--short"]);
69
+ }
70
+
71
+ function canonicalPresets() {
72
+ const configSource = readFileSync(resolve(REPO_ROOT, "extensions/oracle/lib/config.ts"), "utf8");
73
+ const registryMatch = configSource.match(/export const ORACLE_SUBMIT_PRESETS = \{([\s\S]*?)\n\} as const;/);
74
+ if (!registryMatch) throw new Error("Could not locate ORACLE_SUBMIT_PRESETS registry in extensions/oracle/lib/config.ts");
75
+ const entries = [...registryMatch[1].matchAll(
76
+ /^\s{2}([a-z0-9_]+):\s*\{\s*label:\s*"[^"]+",\s*modelFamily:\s*"([a-z]+)"\s+as const(?:,\s*effort:\s*"([a-z]+)"\s+as const)?,\s*autoSwitchToThinking:\s*(true|false)\s*\}/gm,
77
+ )];
78
+ if (entries.length === 0) throw new Error("Could not parse ORACLE_SUBMIT_PRESETS registry entries");
79
+ return Object.fromEntries(entries.map((match) => [match[1], {
80
+ modelFamily: match[2],
81
+ effort: match[3],
82
+ autoSwitchToThinking: match[4] === "true",
83
+ }]));
84
+ }
85
+
86
+ function canonicalPresetIds() {
87
+ return Object.keys(canonicalPresets());
88
+ }
89
+
90
+ function proofPath() {
91
+ return resolve(REPO_ROOT, process.env[PROOF_PATH_ENV] || DEFAULT_PROOF_PATH);
92
+ }
93
+
94
+ function maxAgeHours() {
95
+ const raw = process.env[MAX_AGE_HOURS_ENV];
96
+ if (!raw) return DEFAULT_MAX_AGE_HOURS;
97
+ const parsed = Number(raw);
98
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${MAX_AGE_HOURS_ENV} must be a positive number of hours`);
99
+ return parsed;
100
+ }
101
+
102
+ function isIsoDate(value) {
103
+ if (typeof value !== "string" || !value.trim()) return false;
104
+ const millis = Date.parse(value);
105
+ return Number.isFinite(millis) && new Date(millis).toISOString() === value;
106
+ }
107
+
108
+ function parseIsoMillis(value) {
109
+ return isIsoDate(value) ? Date.parse(value) : undefined;
110
+ }
111
+
112
+ function unique(values) {
113
+ return [...new Set(values.filter(Boolean))];
114
+ }
115
+
116
+ function candidateJobJsonPaths(jobId, proofJob) {
117
+ const paths = [];
118
+ if (typeof proofJob.jobJsonPath === "string" && proofJob.jobJsonPath.trim()) {
119
+ paths.push(resolve(REPO_ROOT, proofJob.jobJsonPath));
120
+ }
121
+ if (typeof proofJob.jobDir === "string" && proofJob.jobDir.trim()) {
122
+ paths.push(resolve(REPO_ROOT, proofJob.jobDir, "job.json"));
123
+ }
124
+ if (process.env[JOBS_DIR_ENV]) {
125
+ paths.push(resolve(process.env[JOBS_DIR_ENV], `oracle-${jobId}`, "job.json"));
126
+ }
127
+ paths.push(resolve("/tmp", `oracle-${jobId}`, "job.json"));
128
+ return unique(paths);
129
+ }
130
+
131
+ function loadOracleJobState(jobId, proofJob) {
132
+ const candidates = candidateJobJsonPaths(jobId, proofJob);
133
+ for (const candidate of candidates) {
134
+ if (!existsSync(candidate)) continue;
135
+ return { path: candidate, state: readJson(candidate) };
136
+ }
137
+ return { path: undefined, state: undefined, candidates };
138
+ }
139
+
140
+ function requireActualJobEvidence({ preset, canonicalPreset, proofJob, packageName, packageVersion, gitHead, gitHeadCommittedAtMs, proofValidatedAtMs, errors }) {
141
+ if (!proofJob || typeof proofJob !== "object" || Array.isArray(proofJob)) {
142
+ errors.push(`missing jobs.${preset}`);
143
+ return;
144
+ }
145
+ if (proofJob.preset !== preset) errors.push(`jobs.${preset}.preset must be ${preset}`);
146
+ if (proofJob.provider !== "chatgpt") errors.push(`jobs.${preset}.provider must be chatgpt`);
147
+
148
+ const jobId = proofJob.jobId;
149
+ if (typeof jobId !== "string" || !UUID_PATTERN.test(jobId) || jobId === ZERO_UUID) {
150
+ errors.push(`jobs.${preset}.jobId must be a real oracle UUID job id, not a placeholder`);
151
+ return;
152
+ }
153
+
154
+ const loaded = loadOracleJobState(jobId, proofJob);
155
+ if (!loaded.state) {
156
+ errors.push(`jobs.${preset} could not find actual oracle job.json for ${jobId}; checked ${loaded.candidates.join(", ")}`);
157
+ return;
158
+ }
159
+
160
+ const state = loaded.state;
161
+ const responsePath = typeof state.responsePath === "string" ? state.responsePath : undefined;
162
+ const workerLogPath = typeof state.workerLogPath === "string" ? state.workerLogPath : undefined;
163
+ const response = responsePath && existsSync(responsePath) ? readFileSync(responsePath, "utf8") : "";
164
+ const workerLog = workerLogPath && existsSync(workerLogPath) ? readFileSync(workerLogPath, "utf8") : "";
165
+ const completedAtMs = parseIsoMillis(state.completedAt || state.phaseAt);
166
+
167
+ if (state.id !== jobId) errors.push(`jobs.${preset} job.json id mismatch: expected ${jobId}, got ${state.id || "<missing>"}`);
168
+ if (state.status !== "complete") errors.push(`jobs.${preset} actual job status must be complete, got ${state.status || "<missing>"}`);
169
+ if (state.phase !== "complete") errors.push(`jobs.${preset} actual job phase must be complete, got ${state.phase || "<missing>"}`);
170
+ if (state.selection?.provider !== "chatgpt") errors.push(`jobs.${preset} actual provider must be chatgpt`);
171
+ if (state.selection?.preset !== preset) errors.push(`jobs.${preset} actual preset must be ${preset}, got ${state.selection?.preset || "<missing>"}`);
172
+ if (state.selection?.modelFamily !== canonicalPreset.modelFamily) errors.push(`jobs.${preset} actual modelFamily must be ${canonicalPreset.modelFamily}, got ${state.selection?.modelFamily || "<missing>"}`);
173
+ if ((state.selection?.effort || undefined) !== canonicalPreset.effort) errors.push(`jobs.${preset} actual effort must be ${canonicalPreset.effort || "<unset>"}, got ${state.selection?.effort || "<unset>"}`);
174
+ if (state.selection?.autoSwitchToThinking !== canonicalPreset.autoSwitchToThinking) errors.push(`jobs.${preset} actual autoSwitchToThinking must be ${canonicalPreset.autoSwitchToThinking}`);
175
+ if (state.cwd !== REPO_ROOT) errors.push(`jobs.${preset} actual cwd must be this repo (${REPO_ROOT}), got ${state.cwd || "<missing>"}`);
176
+ if (state.projectId !== REPO_ROOT) errors.push(`jobs.${preset} actual projectId must be this repo (${REPO_ROOT}), got ${state.projectId || "<missing>"}`);
177
+ if (state.requestSource !== "tool" && state.requestSource !== "command") errors.push(`jobs.${preset} actual requestSource must be tool or command`);
178
+ if (typeof state.sessionId !== "string" || !state.sessionId.trim()) errors.push(`jobs.${preset} actual job must record sessionId`);
179
+ if (typeof state.originSessionFile !== "string" || !existsSync(state.originSessionFile)) errors.push(`jobs.${preset} actual originSessionFile must exist`);
180
+ if (typeof state.promptPath !== "string" || !existsSync(state.promptPath)) errors.push(`jobs.${preset} actual promptPath must exist`);
181
+ if (typeof state.logsDir !== "string" || !existsSync(state.logsDir)) errors.push(`jobs.${preset} actual logsDir must exist`);
182
+ if (typeof state.runtimeId !== "string" || !state.runtimeId.trim()) errors.push(`jobs.${preset} actual job must record runtimeId`);
183
+ if (typeof state.runtimeSessionName !== "string" || !state.runtimeSessionName.trim()) errors.push(`jobs.${preset} actual job must record runtimeSessionName`);
184
+ if (!state.config?.browser || !state.config?.worker || !state.config?.cleanup) errors.push(`jobs.${preset} actual job must include persisted oracle config with browser, worker, and cleanup sections`);
185
+ const lifecycleKinds = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.kind) : []);
186
+ const lifecyclePhases = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.phase) : []);
187
+ if (!lifecycleKinds.has("created")) errors.push(`jobs.${preset} lifecycle events must include job creation`);
188
+ if (!lifecyclePhases.has("configuring_model")) errors.push(`jobs.${preset} lifecycle events must include configuring_model phase`);
189
+ if (!lifecyclePhases.has("complete")) errors.push(`jobs.${preset} lifecycle events must include complete phase`);
190
+ if (state.extensionProvenance?.schemaVersion !== 1) errors.push(`jobs.${preset} actual job must record extensionProvenance.schemaVersion=1`);
191
+ if (state.extensionProvenance?.packageName !== packageName) errors.push(`jobs.${preset} actual extension packageName must be ${packageName}`);
192
+ if (state.extensionProvenance?.packageVersion !== packageVersion) errors.push(`jobs.${preset} actual extension packageVersion must be ${packageVersion}`);
193
+ if (state.extensionProvenance?.gitHead !== gitHead) errors.push(`jobs.${preset} actual extension gitHead must be ${gitHead}`);
194
+ if (state.extensionProvenance?.sourcePath !== REPO_ROOT) errors.push(`jobs.${preset} actual extension sourcePath must be this repo (${REPO_ROOT}), got ${state.extensionProvenance?.sourcePath || "<missing>"}`);
195
+ if (typeof state.archivePath !== "string" || !state.archivePath.endsWith(".tar.zst")) errors.push(`jobs.${preset} actual archivePath must end with .tar.zst`);
196
+ if (typeof state.archiveSha256 !== "string" || !/^[0-9a-f]{64}$/i.test(state.archiveSha256)) errors.push(`jobs.${preset} actual job must record archiveSha256`);
197
+ if (typeof state.conversationId !== "string" || !state.conversationId.trim()) errors.push(`jobs.${preset} actual job must record conversationId`);
198
+ if (typeof state.chatUrl !== "string" || !state.chatUrl.startsWith("https://chatgpt.com/c/")) errors.push(`jobs.${preset} actual job must record a ChatGPT conversation URL`);
199
+ if (!responsePath || !existsSync(responsePath)) errors.push(`jobs.${preset} actual responsePath must exist`);
200
+ if (!workerLogPath || !existsSync(workerLogPath)) errors.push(`jobs.${preset} actual workerLogPath must exist`);
201
+ if (!response.includes(`PRESET ${preset} OK`)) errors.push(`jobs.${preset} actual response must include PRESET ${preset} OK`);
202
+ if (!response.includes(`PACKAGE ${packageName}`)) errors.push(`jobs.${preset} actual response must include PACKAGE ${packageName}`);
203
+ if (!workerLog.includes(`Configuring model family=${state.selection?.modelFamily}`) && !workerLog.includes("Model already appears configured")) {
204
+ errors.push(`jobs.${preset} worker log must show model configuration or an explicit already-configured skip`);
205
+ }
206
+ if (!workerLog.includes("Job completed successfully") && !workerLog.includes(`Job ${jobId} complete`)) errors.push(`jobs.${preset} worker log must show successful completion`);
207
+
208
+ if (completedAtMs === undefined) {
209
+ errors.push(`jobs.${preset} actual completedAt/phaseAt must be an ISO timestamp`);
210
+ } else {
211
+ if (completedAtMs <= gitHeadCommittedAtMs) errors.push(`jobs.${preset} must complete after current git HEAD commit time`);
212
+ if (proofValidatedAtMs !== undefined && completedAtMs > proofValidatedAtMs) errors.push(`jobs.${preset} completed after proof validatedAt`);
213
+ const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
214
+ if (Date.now() - completedAtMs > maxAgeMs) errors.push(`jobs.${preset} completedAt is older than ${maxAgeHours()} hours`);
215
+ }
216
+
217
+ if (typeof proofJob.conversation === "string" && proofJob.conversation.trim() && proofJob.conversation !== state.conversationId && proofJob.conversation !== state.chatUrl) {
218
+ errors.push(`jobs.${preset}.conversation does not match actual conversationId/chatUrl`);
219
+ }
220
+ }
221
+
222
+ function validateProof(proof, path) {
223
+ const errors = [];
224
+ const { name, version } = packageMetadata();
225
+ const gitHead = currentGitHead();
226
+ const gitHeadCommittedAt = currentGitHeadCommittedAt();
227
+ const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
228
+ const gitStatus = currentGitStatus();
229
+ const presetRegistry = canonicalPresets();
230
+ const requiredPresets = Object.keys(presetRegistry);
231
+ const allowedPresets = new Set(requiredPresets);
232
+
233
+ if (gitStatus) {
234
+ errors.push(`working tree must be clean before release proof is accepted; current changes:\n${gitStatus}`);
235
+ }
236
+
237
+ if (!proof || typeof proof !== "object" || Array.isArray(proof)) {
238
+ errors.push("proof root must be a JSON object");
239
+ return errors;
240
+ }
241
+
242
+ if (proof.schemaVersion !== 1) errors.push("schemaVersion must be 1");
243
+ if (proof.packageName !== name) errors.push(`packageName must be ${name}`);
244
+ if (proof.packageVersion !== version) errors.push(`packageVersion must match package.json version ${version}`);
245
+ if (proof.gitHead !== gitHead) errors.push(`gitHead must match current HEAD ${gitHead}`);
246
+ if (proof.provider !== "chatgpt") errors.push('provider must be "chatgpt"');
247
+ if (proof.extensionUnderTest !== "loaded-extension") errors.push('extensionUnderTest must be "loaded-extension"');
248
+
249
+ let proofValidatedAtMs;
250
+ if (!isIsoDate(proof.validatedAt)) {
251
+ errors.push("validatedAt must be an ISO-8601 UTC timestamp from new Date().toISOString()");
252
+ } else {
253
+ proofValidatedAtMs = Date.parse(proof.validatedAt);
254
+ const ageMs = Date.now() - proofValidatedAtMs;
255
+ const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
256
+ if (ageMs < 0) errors.push("validatedAt must not be in the future");
257
+ if (ageMs > maxAgeMs) errors.push(`validatedAt is older than ${maxAgeHours()} hours`);
258
+ if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after current git HEAD commit time");
259
+ }
260
+
261
+ const jobs = proof.jobs;
262
+ if (!jobs || typeof jobs !== "object" || Array.isArray(jobs)) {
263
+ errors.push("jobs must be an object keyed by canonical preset id");
264
+ return errors;
265
+ }
266
+
267
+ for (const preset of requiredPresets) {
268
+ requireActualJobEvidence({
269
+ preset,
270
+ canonicalPreset: presetRegistry[preset],
271
+ proofJob: jobs[preset],
272
+ packageName: name,
273
+ packageVersion: version,
274
+ gitHead,
275
+ gitHeadCommittedAtMs,
276
+ proofValidatedAtMs,
277
+ errors,
278
+ });
279
+ }
280
+
281
+ for (const preset of Object.keys(jobs)) {
282
+ if (!allowedPresets.has(preset)) errors.push(`jobs.${preset} is not a canonical ORACLE_SUBMIT_PRESETS id`);
283
+ }
284
+
285
+ if (errors.length === 0) {
286
+ console.log(`ChatGPT preset release proof accepted: ${path}`);
287
+ console.log(`Validated presets: ${requiredPresets.join(", ")}`);
288
+ }
289
+
290
+ return errors;
291
+ }
292
+
293
+ function template() {
294
+ const { name, version } = packageMetadata();
295
+ const gitHead = currentGitHead();
296
+ const jobs = Object.fromEntries(canonicalPresetIds().map((preset) => [preset, {
297
+ preset,
298
+ provider: "chatgpt",
299
+ jobId: `replace-with-completed-${preset}-job-uuid`,
300
+ jobDir: `/tmp/oracle-replace-with-completed-${preset}-job-uuid`,
301
+ conversation: "replace-with-actual-conversation-id-or-chat-url",
302
+ }]));
303
+
304
+ console.log(JSON.stringify({
305
+ schemaVersion: 1,
306
+ packageName: name,
307
+ packageVersion: version,
308
+ gitHead,
309
+ provider: "chatgpt",
310
+ extensionUnderTest: "loaded-extension",
311
+ validatedAt: new Date().toISOString(),
312
+ jobs,
313
+ }, null, 2));
314
+ }
315
+
316
+ function main() {
317
+ const command = process.argv[2] || "check";
318
+ if (command === "--help" || command === "-h") {
319
+ usage();
320
+ return;
321
+ }
322
+ if (command === "template") {
323
+ template();
324
+ return;
325
+ }
326
+ if (command !== "check") {
327
+ usage();
328
+ fail(`Unknown command: ${command}`);
329
+ return;
330
+ }
331
+
332
+ const path = proofPath();
333
+ if (!existsSync(path)) {
334
+ fail(`Missing ChatGPT preset release proof: ${path}\n\nRun live loaded-extension oracle jobs for every canonical ChatGPT preset, then save proof JSON.\nCreate a non-valid starting template with:\n mkdir -p .artifacts/chatgpt-preset-proof\n node scripts/oracle-chatgpt-preset-proof.mjs template > ${DEFAULT_PROOF_PATH}\n\nThis gate is intentional: releases are blocked until every preset has fresh live proof backed by actual oracle job state.`);
335
+ return;
336
+ }
337
+
338
+ let proof;
339
+ try {
340
+ proof = readJson(path);
341
+ } catch (error) {
342
+ fail(`Could not read proof JSON at ${path}: ${error.message}`);
343
+ return;
344
+ }
345
+
346
+ const errors = validateProof(proof, path);
347
+ if (errors.length > 0) {
348
+ fail(`ChatGPT preset release proof rejected: ${path}\n- ${errors.join("\n- ")}`);
349
+ }
350
+ }
351
+
352
+ main();