pi-oracle 0.7.7 → 0.7.8

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.
@@ -30,12 +30,14 @@ import {
30
30
  effortSelectionVisible,
31
31
  snapshotCanSafelySkipModelConfiguration,
32
32
  snapshotHasModelConfigurationUi,
33
+ snapshotHasModelOpener,
33
34
  snapshotHasUsableComposerControls,
34
35
  snapshotStronglyMatchesRequestedModel,
35
36
  snapshotWeaklyMatchesRequestedModel,
36
37
  autoSwitchToThinkingSelectionVisible,
38
+ stripChatGptResponseChrome,
37
39
  } from "./chatgpt-ui-helpers.mjs";
38
- import { assistantSnapshotSlice, nextStableValueState, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
40
+ import { assistantSnapshotSlice, nextStableValueState, providerSendAccepted, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
39
41
  import { assertNotKnownBrowserUserDataPath, scrubSweetCookieSafeStoragePasswordEnv, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
40
42
  import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
41
43
 
@@ -86,6 +88,7 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
86
88
  const CP_BIN = process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
87
89
  scrubSweetCookieSafeStoragePasswordEnv();
88
90
 
91
+ let cpSupportsApfsCloneFlag;
89
92
  let currentJob;
90
93
  let browserStarted = false;
91
94
  let cleaningUpBrowser = false;
@@ -277,6 +280,14 @@ function spawnCommand(command, args, options = {}) {
277
280
  });
278
281
  }
279
282
 
283
+ async function cpSupportsApfsClone() {
284
+ if (process.platform !== "darwin") return false;
285
+ if (cpSupportsApfsCloneFlag !== undefined) return cpSupportsApfsCloneFlag;
286
+ const probe = await spawnCommand(CP_BIN, ["-c"], { allowFailure: true, timeoutMs: 5_000 });
287
+ cpSupportsApfsCloneFlag = !/invalid option\s+--\s+['"]?c/i.test(`${probe.stderr}\n${probe.stdout}`);
288
+ return cpSupportsApfsCloneFlag;
289
+ }
290
+
280
291
  function parseConversationId(chatUrl) {
281
292
  if (!chatUrl) return undefined;
282
293
  try {
@@ -321,7 +332,7 @@ async function cloneSeedProfileToRuntime(job) {
321
332
  await withLock(ORACLE_STATE_DIR, "auth", "global", { jobId: job.id, processPid: process.pid, action: "cloneSeedProfile" }, async () => {
322
333
  await rm(job.runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
323
334
  await ensurePrivateDir(dirname(job.runtimeProfileDir));
324
- if (job.config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
335
+ if (job.config.browser.cloneStrategy === "apfs-clone" && await cpSupportsApfsClone()) {
325
336
  try {
326
337
  await spawnCommand(CP_BIN, ["-cR", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
327
338
  } catch (error) {
@@ -774,8 +785,8 @@ function snapshotHasCompactIntelligenceMenuControls(snapshot) {
774
785
  return Boolean(findEntry(snapshot, (candidate) => {
775
786
  if (candidate.disabled) return false;
776
787
  const label = normalizeSnapshotLabel(candidate.label);
777
- return (candidate.kind === "menu" && /Intelligence.*Instant.*Medium.*High.*Pro/i.test(label))
778
- || (candidate.kind === "menuitemradio" && /^(?:Instant\s+5s|Medium\s+5\s*[–-]\s*30s|High\s+15\s*[–-]\s*60s|Pro\s+5\+\s*min)$/i.test(label));
788
+ return (candidate.kind === "menu" && /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Extended)$)/i.test(label))
789
+ || (candidate.kind === "menuitemradio" && /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro\s+5\+\s*min|Pro Standard|Pro Extended)$/i.test(label));
779
790
  }));
780
791
  }
781
792
 
@@ -783,9 +794,10 @@ function matchesRequestedModelControl(candidate, selection, options = {}) {
783
794
  if (!["button", "radio", "menuitemradio"].includes(candidate.kind || "") || typeof candidate.label !== "string" || candidate.disabled) return false;
784
795
  if (candidate.kind === "button") {
785
796
  if (/\bexpanded=true\b/.test(String(candidate.line || ""))) return false;
786
- if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Pro)$/i.test(candidate.label)) return false;
787
- if (options.ignoreCompactOnlyButtons && /^(?:Medium|High)$/i.test(candidate.label)) return false;
797
+ if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Extra High|Pro|Pro Extended)$/i.test(candidate.label)) return false;
798
+ if (options.ignoreCompactOnlyButtons && /^(?:Medium|High|Extra High)$/i.test(candidate.label)) return false;
788
799
  }
800
+ if (selection.modelFamily === "pro" && /^Pro(?:\s+Extended)?$/i.test(candidate.label)) return true;
789
801
  return matchesRequestedModelControlLabel(candidate.label, selection);
790
802
  }
791
803
 
@@ -859,7 +871,41 @@ async function maybeClickLabeledEntry(job, label, options = {}) {
859
871
  }
860
872
 
861
873
  async function openEffortDropdown(job) {
862
- const snapshot = await snapshotText(job);
874
+ let snapshot = await snapshotText(job);
875
+ if (job.selection?.modelFamily === "pro") {
876
+ let proEffortEntry = findEntry(
877
+ snapshot,
878
+ (candidate) => candidate.kind === "menuitem" && candidate.label === "Pro effort options" && !candidate.disabled,
879
+ );
880
+ if (!proEffortEntry) {
881
+ const opener = findEntry(snapshot, matchesModelConfigurationOpener);
882
+ if (opener) {
883
+ await clickRef(job, opener.ref);
884
+ await agentBrowser(job, "wait", "500");
885
+ snapshot = await snapshotText(job);
886
+ proEffortEntry = findEntry(
887
+ snapshot,
888
+ (candidate) => candidate.kind === "menuitem" && candidate.label === "Pro effort options" && !candidate.disabled,
889
+ );
890
+ }
891
+ }
892
+ if (proEffortEntry) {
893
+ try {
894
+ await clickRef(job, proEffortEntry.ref);
895
+ return true;
896
+ } catch {
897
+ // Fall through to DOM click. ChatGPT's tiny trailing Pro effort icon can
898
+ // be covered at the accessibility click point by the parent Pro row.
899
+ }
900
+ }
901
+ const clicked = await evalPage(job, toJsonScript(`
902
+ const el = document.querySelector('[aria-label="Pro effort options"], [data-composer-intelligence-pro-effort-action]');
903
+ if (!el) return false;
904
+ el.click();
905
+ return true;
906
+ `));
907
+ if (clicked) return true;
908
+ }
863
909
  const effortLabels = new Set(["Light", "Standard", "Extended", "Heavy"]);
864
910
  const entry = findEntry(
865
911
  snapshot,
@@ -1157,46 +1203,128 @@ async function waitForSendReady(job) {
1157
1203
  throw new Error(`Timed out waiting for ${labelsForJob(job).send} to become enabled`);
1158
1204
  }
1159
1205
 
1160
- async function clickSend(job) {
1161
- const entry = await waitForSendReady(job);
1162
- if (isGrokJob(job)) {
1163
- const result = await evalPage(job, toJsonScript(`
1164
- const button = document.querySelector('button[aria-label="Submit"]');
1165
- if (!button || button.disabled) return { ok: false };
1166
- button.click();
1167
- return { ok: true };
1168
- `));
1169
- if (result?.ok) return;
1206
+ async function activateSendButton(job) {
1207
+ const result = await evalPage(job, toJsonScript(`
1208
+ const labels = ${JSON.stringify(labelsForJob(job))};
1209
+ const buttons = Array.from(document.querySelectorAll('button'));
1210
+ const button = buttons.find((candidate) => {
1211
+ const label = (candidate.getAttribute('aria-label') || candidate.textContent || '').trim();
1212
+ return label === labels.send;
1213
+ });
1214
+ if (!button) return { ok: false, reason: 'send button not found' };
1215
+ if (button.disabled || button.getAttribute('aria-disabled') === 'true') return { ok: false, reason: 'send button disabled' };
1216
+ button.click();
1217
+ return { ok: true };
1218
+ `));
1219
+ return result;
1220
+ }
1221
+
1222
+ async function sendAcceptanceState(job, baselineAssistantCount) {
1223
+ const [urlResult, snapshot, messages] = await Promise.all([
1224
+ currentUrl(job).then((url) => ({ url, ok: true })).catch(() => ({ url: "", ok: false })),
1225
+ snapshotText(job).catch(() => ""),
1226
+ assistantMessages(job).catch(() => []),
1227
+ ]);
1228
+ return {
1229
+ url: urlResult.url,
1230
+ urlKnown: urlResult.ok,
1231
+ assistantCount: Math.max(baselineAssistantCount, messages.length),
1232
+ stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
1233
+ };
1234
+ }
1235
+
1236
+ async function clickSend(job, baselineAssistantCount) {
1237
+ await waitForSendReady(job);
1238
+ const beforeSend = await sendAcceptanceState(job, baselineAssistantCount);
1239
+ const activation = await activateSendButton(job);
1240
+ if (!activation?.ok) throw new Error(`Could not activate ${labelsForJob(job).send}: ${activation?.reason || "DOM activation failed"}`);
1241
+ await log(`Activated ${labelsForJob(job).send}; waiting for provider acceptance evidence`);
1242
+ if (await waitForSendAccepted(job, beforeSend, { timeoutMs: 20_000 })) return;
1243
+
1244
+ await captureDiagnostics(job, "send-not-accepted");
1245
+ throw new Error(`${isGrokJob(job) ? "Grok" : "ChatGPT"} message did not leave the composer after activating ${labelsForJob(job).send}`);
1246
+ }
1247
+
1248
+ async function waitForSendAccepted(job, beforeSend, options = {}) {
1249
+ const timeoutAt = Date.now() + (options.timeoutMs || 15_000);
1250
+ while (Date.now() < timeoutAt) {
1251
+ await heartbeat();
1252
+ const afterSend = await sendAcceptanceState(job, beforeSend.assistantCount || 0);
1253
+ if (providerSendAccepted(beforeSend, afterSend)) return true;
1254
+ await sleep(500);
1170
1255
  }
1171
- await clickRef(job, entry.ref);
1256
+ return false;
1257
+ }
1258
+
1259
+ async function dismissProFeedbackModal(job, snapshot) {
1260
+ const entries = parseSnapshotEntries(snapshot);
1261
+ const hasProFeedback = entries.some((entry) => entry.kind === "heading" && entry.label === "Pro feedback" && !entry.disabled);
1262
+ if (!hasProFeedback) return false;
1263
+ const close = entries.find((entry) => entry.kind === "button" && entry.label === CHATGPT_LABELS.close && !entry.disabled);
1264
+ if (close) {
1265
+ await clickRef(job, close.ref).catch(() => undefined);
1266
+ await agentBrowser(job, "wait", "500");
1267
+ if (!(await pageText(job).catch(() => "")).includes("Pro feedback")) return true;
1268
+ }
1269
+ await agentBrowser(job, "press", "Escape").catch(() => undefined);
1270
+ await agentBrowser(job, "wait", "500");
1271
+ if (!(await pageText(job).catch(() => "")).includes("Pro feedback")) return true;
1272
+
1273
+ const dismissed = await evalPage(job, toJsonScript(`
1274
+ const dialogText = document.body.innerText || '';
1275
+ if (!/Pro feedback/.test(dialogText)) return false;
1276
+ const button = Array.from(document.querySelectorAll('button'))
1277
+ .find((candidate) => (candidate.getAttribute('aria-label') || candidate.textContent || '').trim() === 'Close');
1278
+ if (!button) return false;
1279
+ button.click();
1280
+ return true;
1281
+ `));
1282
+ if (dismissed) await agentBrowser(job, "wait", "500");
1283
+ return Boolean(dismissed);
1172
1284
  }
1173
1285
 
1174
1286
  async function openModelConfiguration(job) {
1175
- const initialSnapshot = await snapshotText(job);
1176
- if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
1287
+ const timeoutAt = Date.now() + 15_000;
1288
+ let lastSnapshot = "";
1177
1289
 
1178
- for (const predicate of [matchesModelConfigurationOpener]) {
1179
- const snapshot = await snapshotText(job);
1180
- const entry = findEntry(snapshot, predicate);
1181
- if (!entry) continue;
1182
- await clickRef(job, entry.ref);
1183
- await agentBrowser(job, "wait", "800");
1184
- const after = await snapshotText(job);
1185
- if (snapshotHasModelConfigurationUi(after)) return after;
1186
- if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
1290
+ while (Date.now() < timeoutAt) {
1291
+ const initialSnapshot = await snapshotText(job);
1292
+ lastSnapshot = initialSnapshot;
1293
+ if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
1294
+ if (await dismissProFeedbackModal(job, initialSnapshot)) continue;
1295
+
1296
+ for (const predicate of [matchesModelConfigurationOpener]) {
1297
+ const snapshot = await snapshotText(job);
1298
+ lastSnapshot = snapshot;
1299
+ const entry = findEntry(snapshot, predicate);
1300
+ if (!entry) continue;
1301
+ await clickRef(job, entry.ref);
1302
+ await agentBrowser(job, "wait", "800");
1303
+ const after = await snapshotText(job);
1304
+ lastSnapshot = after;
1305
+ if (snapshotHasModelConfigurationUi(after)) return after;
1306
+ if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
1307
+
1308
+ const configureEntry = findEntry(
1309
+ after,
1310
+ (candidate) => candidate.kind === "menuitem" && candidate.label === CHATGPT_LABELS.configure && !candidate.disabled,
1311
+ );
1187
1312
 
1188
- const configureEntry = findEntry(
1189
- after,
1190
- (candidate) => candidate.kind === "menuitem" && candidate.label === CHATGPT_LABELS.configure && !candidate.disabled,
1191
- );
1313
+ if (configureEntry) {
1314
+ await clickRef(job, configureEntry.ref);
1315
+ await agentBrowser(job, "wait", "1200");
1316
+ const postConfigure = await snapshotText(job);
1317
+ lastSnapshot = postConfigure;
1318
+ if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
1319
+ if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
1320
+ }
1321
+ }
1192
1322
 
1193
- if (configureEntry) {
1194
- await clickRef(job, configureEntry.ref);
1195
- await agentBrowser(job, "wait", "1200");
1196
- const postConfigure = await snapshotText(job);
1197
- if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
1198
- if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
1323
+ if (composerControlsVisible(lastSnapshot, job) && !snapshotHasModelOpener(lastSnapshot)) {
1324
+ await agentBrowser(job, "wait", "1000");
1325
+ continue;
1199
1326
  }
1327
+ await agentBrowser(job, "wait", "500");
1200
1328
  }
1201
1329
 
1202
1330
  throw new Error("Could not open model configuration UI");
@@ -1303,7 +1431,11 @@ async function configureModel(job) {
1303
1431
  throw new Error(`Could not open effort dropdown for requested effort: ${effortLabel}`);
1304
1432
  }
1305
1433
  await agentBrowser(job, "wait", "300");
1306
- await clickLabeledEntry(job, effortLabel, { kind: "option" });
1434
+ if (job.selection.modelFamily === "pro" && await maybeClickLabeledEntry(job, `Pro ${effortLabel}`, { kind: "menuitemradio" })) {
1435
+ // Current ChatGPT exposes Pro effort choices as nested menu radio items.
1436
+ } else {
1437
+ await clickLabeledEntry(job, effortLabel, { kind: "option" });
1438
+ }
1307
1439
  await agentBrowser(job, "wait", "400");
1308
1440
  const effortSnapshot = await snapshotText(job);
1309
1441
  verificationSnapshot = effortSnapshot;
@@ -2000,15 +2132,8 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
2000
2132
  }
2001
2133
  }
2002
2134
 
2003
- const capturedArtifactLabels = new Set(artifacts.map((artifact) => artifact.displayName).filter(Boolean));
2004
- const capturedArtifactKeys = new Set([...capturedArtifactLabels].map((label) => String(label).replace(/\s+/g, "")));
2005
- const missedArtifactLabels = suspiciousLabels.filter((label) => !capturedArtifactLabels.has(label) && !capturedArtifactKeys.has(String(label).replace(/\s+/g, "")));
2006
- if (missedArtifactLabels.length > 0) {
2007
- await log(`Marking missed artifact signals as unconfirmed: ${missedArtifactLabels.join(", ")}`);
2008
- for (const label of missedArtifactLabels) {
2009
- artifacts.push({ displayName: label, unconfirmed: true, error: "Response-local artifact signal was present, but no downloadable artifact was captured." });
2010
- }
2011
- await flushArtifactsState(artifacts);
2135
+ if (suspiciousLabels.length > 0) {
2136
+ await log(`Ignoring plain-text artifact-like labels without downloadable controls: ${suspiciousLabels.join(", ")}`);
2012
2137
  }
2013
2138
 
2014
2139
  return artifacts;
@@ -2079,8 +2204,8 @@ async function run() {
2079
2204
  await setComposerText(currentJob, await readFile(currentJob.promptPath, "utf8"));
2080
2205
  const baselineAssistantCount = (await assistantMessages(currentJob)).length;
2081
2206
  await log(`Assistant response count before send: ${baselineAssistantCount}`);
2082
- await clickSend(currentJob);
2083
- await log(`Waiting ${POST_SEND_SETTLE_MS}ms after send to avoid streaming interruption`);
2207
+ await clickSend(currentJob, baselineAssistantCount);
2208
+ await log(`Send accepted; waiting ${POST_SEND_SETTLE_MS}ms after send to avoid streaming interruption`);
2084
2209
  await sleep(POST_SEND_SETTLE_MS);
2085
2210
 
2086
2211
  const observedChatUrl = isGrokJob(currentJob)
@@ -2118,14 +2243,15 @@ async function run() {
2118
2243
  message: "Extracting the completed response body.",
2119
2244
  patch: { heartbeatAt: new Date().toISOString() },
2120
2245
  }));
2121
- await secureWriteText(currentJob.responsePath, `${completion.responseText.trim()}\n`);
2246
+ const responseText = isGrokJob(currentJob) ? completion.responseText.trim() : stripChatGptResponseChrome(completion.responseText);
2247
+ await secureWriteText(currentJob.responsePath, `${responseText}\n`);
2122
2248
  currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "downloading_artifacts", {
2123
2249
  at: new Date().toISOString(),
2124
2250
  source: "oracle:worker",
2125
2251
  message: "Downloading any response artifacts.",
2126
2252
  patch: { heartbeatAt: new Date().toISOString() },
2127
2253
  }));
2128
- const artifacts = await downloadArtifacts(currentJob, completion.responseIndex, completion.responseText);
2254
+ const artifacts = await downloadArtifacts(currentJob, completion.responseIndex, responseText);
2129
2255
  const artifactFailureCount = artifacts.filter((artifact) => artifact.unconfirmed || artifact.error).length;
2130
2256
  const finalPhase = artifactFailureCount > 0 ? "complete_with_artifact_errors" : "complete";
2131
2257
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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",
@@ -41,9 +41,6 @@
41
41
  "pi": {
42
42
  "extensions": [
43
43
  "./extensions/oracle/index.ts"
44
- ],
45
- "prompts": [
46
- "./prompts"
47
44
  ]
48
45
  },
49
46
  "scripts": {
@@ -83,8 +80,8 @@
83
80
  "protobufjs": "7.6.1"
84
81
  },
85
82
  "devDependencies": {
86
- "@earendil-works/pi-ai": "^0.79.0",
87
- "@earendil-works/pi-coding-agent": "^0.79.0",
83
+ "@earendil-works/pi-ai": "0.79.1",
84
+ "@earendil-works/pi-coding-agent": "0.79.1",
88
85
  "@types/node": "^22.19.19",
89
86
  "esbuild": "^0.28.0",
90
87
  "tsx": "^4.22.3",
@@ -10,7 +10,7 @@ Required workflow:
10
10
  2. If the request does not include both a prior oracle job id and a follow-up request, stop and report: `Usage: /oracle-followup <job-id> <request>. Find the job id in the earlier oracle response or via /oracle-status.`
11
11
  3. Same-thread follow-ups cannot switch providers. If the user asks to move a ChatGPT thread to Grok or a Grok thread to ChatGPT, stop and tell them to start a new `/oracle` job instead.
12
12
  4. Call `oracle_preflight` immediately with the parsed `followUpJobId` so readiness checks use the prior job's provider.
13
- 5. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, or prepare archive inputs first. If the blocker is an auth seed or stale-auth issue that `oracle_auth` can repair, call `oracle_auth` once for the provider reported by preflight, rerun `oracle_preflight` with the same `followUpJobId`, and continue only if it becomes `ready: true`. Otherwise stop immediately and report the blocking issue plus the suggested next step.
13
+ 5. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
14
14
  6. Treat the parsed job id as `followUpJobId` for `oracle_submit`.
15
15
  7. Understand whether the follow-up request is explicitly narrow or genuinely broad.
16
16
  8. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MB for ChatGPT, 200 MiB for Grok.
@@ -31,7 +31,7 @@ Oracle provider/model (`oracle_submit`):
31
31
 
32
32
  Rules:
33
33
  - Use `oracle_preflight` before any expensive `/oracle-followup` preparation so missing persisted-session or local auth/config blockers fail fast.
34
- - If the immediately preceding oracle run for this follow-up failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, call `oracle_auth` once and then retry the follow-up submission once. For Grok, pass `provider: "grok"` to `oracle_auth`. Do not loop auth refreshes.
34
+ - If the immediately preceding oracle run for this follow-up failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle-followup`.
35
35
  - This prompt exists so normal users can continue the same provider thread without manually constructing `followUpJobId` tool calls.
36
36
  - Always include an archive. Do not submit without context files.
37
37
  - By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MB for ChatGPT and 200 MiB for Grok. For broad or unclear follow-up requests, include the whole repository by passing `.`. Default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like `.env` files, key material, credential dotfiles, local database files, and nested `secrets/` directories anywhere in the repo.
package/prompts/oracle.md CHANGED
@@ -5,20 +5,28 @@ You are preparing an /oracle job.
5
5
 
6
6
  Do not answer the user's request directly yet.
7
7
 
8
+ Hard requirements:
9
+ - Do not plan instead of submitting. The point of `/oracle` is dispatch.
10
+ - Do not claim preflight, auth, archive prep, or submission happened unless the matching tool call actually happened in this turn.
11
+ - If the user explicitly says ChatGPT Instant or Instant, use provider `chatgpt` and preset `instant`; never switch that request to Grok.
12
+ - If a required tool call is unavailable or fails, stop and report that exact blocker instead of fabricating progress.
13
+ - After a successful or queued `oracle_submit`, your final answer must be only a terse dispatch summary with the job id and response path. Do not ask questions, offer to watch/poll/read, list next steps, or continue working.
14
+
8
15
  Required workflow:
9
- 1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`.
10
- 2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, or prepare archive inputs first. If the blocker is an auth seed or stale-auth issue that `oracle_auth` can repair, call `oracle_auth` once with the same provider, rerun `oracle_preflight` with the same provider, and continue only if it becomes `ready: true`. Otherwise stop immediately and report the blocking issue plus the suggested next step.
16
+ 1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`. If the user says to use ChatGPT, pass `provider: "chatgpt"`. If the user says to use ChatGPT Instant, pass `provider: "chatgpt"` and later call `oracle_submit` with `preset: "instant"`.
17
+ 2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
11
18
  3. Understand the request and decide whether it is explicitly narrow or genuinely broad.
12
19
  4. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MB for ChatGPT, 200 MiB for Grok.
13
20
  5. If the user scope is explicit and narrow, start from the directly relevant area but still include nearby files, tests, docs, configs, and adjacent modules when they may improve answer quality. Keep the archive tightly minimal only when the user explicitly asks for that, privacy/sensitivity requires it, or size pressure forces it.
14
21
  6. If the request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
15
22
  7. Choose archive inputs for the oracle job.
16
23
  8. Craft a concise but complete oracle prompt for the selected web provider.
17
- 9. Call `oracle_submit` with the prompt and exact archive inputs.
18
- 10. Stop immediately after dispatching the oracle job.
24
+ 9. Call `oracle_submit` with the prompt and exact archive inputs. Do not ask for confirmation before this submit step unless `oracle_preflight` or `oracle_submit` returns a blocker that requires user action.
25
+ 10. Stop immediately after dispatching the oracle job. “Stop” means no follow-up questions, no offers to poll/watch/read, and no extra next-step list.
19
26
 
20
27
  Oracle provider/model (`oracle_submit`):
21
28
  - If the user says to use Grok (for example “Use the oracle to Grok about ...”), pass **`provider: "grok"`**. Grok currently supports only **`mode: "heavy"`**; omit `mode` unless the user explicitly says Heavy.
29
+ - If the user says ChatGPT, pass **`provider: "chatgpt"`**. Never route a ChatGPT request to Grok.
22
30
  - Otherwise omit **`provider`** to use the configured default provider, or pass **`provider: "chatgpt"`** only when needed for clarity.
23
31
  - To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the canonical preset registry.
24
32
  - Matching human-readable preset labels and common hyphen/space variants are also accepted and normalized automatically, but prefer canonical ids when readily available.
@@ -28,7 +36,7 @@ Oracle provider/model (`oracle_submit`):
28
36
 
29
37
  Rules:
30
38
  - Use `oracle_preflight` before any expensive `/oracle` preparation so missing persisted-session or local auth/config blockers fail fast.
31
- - If the immediately preceding oracle run for this request failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, call `oracle_auth` once and then retry the oracle submission once. For Grok, pass `provider: "grok"` to `oracle_auth`. Do not loop auth refreshes.
39
+ - If ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle`.
32
40
  - Always include an archive. Do not submit without context files.
33
41
  - By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MB for ChatGPT and 200 MiB for Grok. For broad or unclear requests, include the whole repository by passing `.`. Default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like `.env` files, key material, credential dotfiles, local database files, and nested `secrets/` directories anywhere in the repo.
34
42
  - Only limit file selection if the user explicitly requests a tight archive, if privacy/sensitivity requires it, or if the archive would otherwise exceed the size limit after exclusions/pruning.
@@ -397,7 +397,9 @@ async function run(mode = "packed") {
397
397
  const agent1 = join(runDir, "agent1");
398
398
  const sessions1 = join(runDir, "sessions1");
399
399
  const jobs1 = join(runDir, "jobs1");
400
- mkdirSync(join(agent1, "extensions", "oracle-auth-seed-profile"), { recursive: true });
400
+ const authSeed1 = join(agent1, "extensions", "oracle-auth-seed-profile");
401
+ mkdirSync(authSeed1, { recursive: true });
402
+ writeFileSync(join(authSeed1, ".oracle-seed-generation"), "real-smoke-fake-worker\n");
401
403
  mkdirSync(sessions1, { recursive: true });
402
404
  mkdirSync(jobs1, { recursive: true });
403
405
 
@@ -444,7 +446,9 @@ async function run(mode = "packed") {
444
446
  const sessions2 = join(runDir, "sessions2");
445
447
  const jobs2 = join(runDir, "jobs2");
446
448
  const outside = join(tmpRoot, "outside");
447
- mkdirSync(join(agent2, "extensions", "oracle-auth-seed-profile"), { recursive: true });
449
+ const authSeed2 = join(agent2, "extensions", "oracle-auth-seed-profile");
450
+ mkdirSync(authSeed2, { recursive: true });
451
+ writeFileSync(join(authSeed2, ".oracle-seed-generation"), "real-smoke-fake-worker\n");
448
452
  mkdirSync(sessions2, { recursive: true });
449
453
  mkdirSync(jobs2, { recursive: true });
450
454
  mkdirSync(outside, { recursive: true });