pi-oracle 0.7.14 → 0.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.16 - 2026-06-24
6
+
7
+ ### Changed
8
+ - updated the local pi development and validation baseline to `@earendil-works/*` `0.80.2`
9
+ - exposed packaged `/oracle` and `/oracle-followup` prompt templates through the package manifest so TUI slash completion can discover them
10
+ - reduced always-on `oracle_submit` prompt metadata while keeping detailed dispatch rules in the prompt templates
11
+
12
+ ### Fixed
13
+ - made `/oracle-*` command output visible in JSON mode through a displayed custom message instead of UI notifications only
14
+ - deferred Chrome user-agent probing until oracle config is loaded, with a timeout on the Chrome version probe
15
+
16
+ ## 0.7.15 - 2026-06-23
17
+
18
+ ### Changed
19
+ - updated the local pi development and validation baseline to `@earendil-works/*` `0.80.1`
20
+ - refreshed oracle README and sanity contracts for pi `0.80.1`
21
+ - moved script-only `@earendil-works/pi-ai` type imports to `@earendil-works/pi-ai/compat`, matching the Pi 0.80 source typechecking migration guidance
22
+
23
+ ### Compatibility
24
+ - reviewed the pi `0.80.0` and `0.80.1` changelogs plus current extension lifecycle, project-trust, security, and package docs; no oracle runtime behavior change was required
25
+
26
+ ### Validation
27
+ - ran `npm run release:check`, including `npm run verify:oracle`, fresh live ChatGPT preset proof for every canonical preset, and Crabbox macOS, Ubuntu, and Windows native `platform-build` plus `real-extension` suites
28
+
5
29
  ## 0.7.14 - 2026-06-22
6
30
 
7
31
  ### Changed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
4
4
 
5
- > Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.10`. Pi `0.79.10+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
5
+ > Status: experimental public beta. Current local validation baseline is pi `0.80.2`; the platform-smoke harness covers macOS, Linux, and Windows native with Chromium-family browsers. Pi `0.80.2+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
6
6
 
7
7
  ## What a successful run looks like
8
8
 
@@ -71,13 +71,15 @@ Or from GitHub:
71
71
  pi install https://github.com/fitchmultz/pi-oracle
72
72
  ```
73
73
 
74
+ To update the package later, use `pi update --extensions`, `pi update --all`, or `pi update npm:pi-oracle`. Bare `pi update` updates pi itself only. Versioned npm/git refs stay pinned until you change the configured source.
75
+
74
76
  ### 2. Check requirements
75
77
 
76
78
  You need:
77
79
 
78
80
  - macOS, Linux, or Windows native
79
81
  - Node.js 22 or newer
80
- - Suggested tested floor: `pi` 0.79.10 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
82
+ - Suggested tested floor: `pi` 0.80.2 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
81
83
  - Google Chrome/Chromium or another Chromium-family browser
82
84
  - ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
83
85
  - `agent-browser` and `tar` available on the machine; `zstd` is also required when submitting ChatGPT `.tar.zst` archives
@@ -7,7 +7,7 @@ Companion doc:
7
7
  - `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
8
8
 
9
9
  Compatibility target:
10
- - `pi` 0.79.10+ is the suggested tested floor for current project-trust-aware package/runtime validation
10
+ - `pi` 0.80.2+ is the suggested tested floor for current project-trust-aware package/runtime validation
11
11
  - package metadata keeps pi runtime packages as optional wildcard peers, so this suggested floor is not enforced as a hard npm install requirement
12
12
  - current extension lifecycle only; no backward-compatibility shims for removed `session_switch` / `session_fork` events
13
13
 
@@ -639,6 +639,9 @@ Remaining non-blocking hardening work:
639
639
  - keep hardening model-selection verification against future ChatGPT UI variation
640
640
 
641
641
  Recent proof points:
642
+ - Pi 0.80.2 local gate: `npm run verify:oracle` passed on 2026-06-24 after the JSON command output, prompt-manifest, schema, and lazy Chrome-probe audit fixes
643
+ - Pi 0.80.2 isolated extension smokes: `.artifacts/real-smoke/run-1782321054924-jnq0x3` passed source proof, and `.artifacts/real-smoke/run-1782321056224-yuq5a2` passed packed-install proof
644
+ - Pi 0.80.2 JSON command smoke: `pi --no-extensions -e ./extensions/oracle/index.ts --mode json --no-session --no-approve "/oracle-status"` emitted displayed `oracle-command-output` JSON events
642
645
  - Pi 0.79.10 local gate: `npm run verify:oracle` passed on 2026-06-22 after the 0.79.10 baseline refresh and `CONFIG_DIR_NAME` cleanup
643
646
  - Pi 0.79.10 isolated extension smokes: `.artifacts/real-smoke/run-1782137209549-0xe67z` passed packed-install proof, and `.artifacts/real-smoke/run-1782137217821-95a1po` passed source model-agent proof
644
647
  - Pi 0.79.10 platform artifacts: `.artifacts/platform-smoke/run-1782137574391-7lay68` (macOS platform-build), `.artifacts/platform-smoke/run-1782137619352-gku7jz` (macOS real-extension), `.artifacts/platform-smoke/run-1782137587082-d7kg4p` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1782137619176-lgxezy` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1782137625964-66z0oc` (Windows native platform-build), `.artifacts/platform-smoke/run-1782137752969-pbmdj1` (Windows native real-extension)
@@ -83,12 +83,21 @@ function isProjectTrusted(ctx: ExtensionCommandContext): boolean {
83
83
  return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
84
84
  }
85
85
 
86
- function emitCommandOutput(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
86
+ function emitCommandOutput(pi: ExtensionAPI, ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
87
87
  if (ctx.mode === "print") {
88
88
  process.stdout.write(`${message}\n`);
89
89
  return;
90
90
  }
91
- ctx.ui.notify(message, level);
91
+ if (ctx.hasUI) {
92
+ ctx.ui.notify(message, level);
93
+ return;
94
+ }
95
+ pi.sendMessage({
96
+ customType: "oracle-command-output",
97
+ content: message,
98
+ display: true,
99
+ details: { level },
100
+ });
92
101
  }
93
102
 
94
103
  export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string, _promptTemplates: OracleCommandPromptTemplates = {}): void {
@@ -98,11 +107,11 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
98
107
  try {
99
108
  const provider = parseOracleAuthProvider(args);
100
109
  const providerLabel = provider === "grok" ? "Grok" : provider === "chatgpt" ? "ChatGPT" : "configured provider";
101
- emitCommandOutput(ctx, `Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
110
+ emitCommandOutput(pi, ctx, `Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
102
111
  const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd, provider, { projectConfigTrusted: isProjectTrusted(ctx) });
103
- emitCommandOutput(ctx, result, "info");
112
+ emitCommandOutput(pi, ctx, result, "info");
104
113
  } catch (error) {
105
- emitCommandOutput(ctx, error instanceof Error ? error.message : String(error), "warning");
114
+ emitCommandOutput(pi, ctx, error instanceof Error ? error.message : String(error), "warning");
106
115
  }
107
116
  },
108
117
  });
@@ -113,12 +122,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
113
122
  const explicitJobId = args.trim();
114
123
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
115
124
  if (!jobId) {
116
- emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
125
+ emitCommandOutput(pi, ctx, "No oracle jobs found for this project", "info");
117
126
  return;
118
127
  }
119
128
  const job = readScopedJob(jobId, ctx.cwd);
120
129
  if (!job) {
121
- emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
130
+ emitCommandOutput(pi, ctx, `Oracle job ${jobId} was not found in this project`, "warning");
122
131
  return;
123
132
  }
124
133
  if (isTerminalOracleJob(job)) {
@@ -130,7 +139,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
130
139
  }
131
140
  const summary = await summarizeJob(job.id);
132
141
  const recentJobs = !explicitJobId ? listRecentJobIds(ctx.cwd) : undefined;
133
- emitCommandOutput(ctx, [summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
142
+ emitCommandOutput(pi, ctx, [summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
134
143
  },
135
144
  });
136
145
 
@@ -140,12 +149,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
140
149
  const explicitJobId = args.trim();
141
150
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
142
151
  if (!jobId) {
143
- emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
152
+ emitCommandOutput(pi, ctx, "No oracle jobs found for this project", "info");
144
153
  return;
145
154
  }
146
155
  const job = readScopedJob(jobId, ctx.cwd);
147
156
  if (!job) {
148
- emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
157
+ emitCommandOutput(pi, ctx, `Oracle job ${jobId} was not found in this project`, "warning");
149
158
  return;
150
159
  }
151
160
  if (isTerminalOracleJob(job)) {
@@ -155,7 +164,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
155
164
  cwd: ctx.cwd,
156
165
  });
157
166
  }
158
- emitCommandOutput(ctx, await summarizeJob(job.id, { responsePreview: true }), "info");
167
+ emitCommandOutput(pi, ctx, await summarizeJob(job.id, { responsePreview: true }), "info");
159
168
  },
160
169
  });
161
170
 
@@ -164,20 +173,20 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
164
173
  handler: async (args, ctx) => {
165
174
  const jobId = args.trim();
166
175
  if (!jobId) {
167
- emitCommandOutput(ctx, "Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
176
+ emitCommandOutput(pi, ctx, "Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
168
177
  return;
169
178
  }
170
179
 
171
180
  const job = readScopedJob(jobId, ctx.cwd);
172
181
  if (!job) {
173
- emitCommandOutput(ctx, `Oracle job ${jobId} not found in this project`, "warning");
182
+ emitCommandOutput(pi, ctx, `Oracle job ${jobId} not found in this project`, "warning");
174
183
  return;
175
184
  }
176
185
  if (!isOpenOracleJob(job)) {
177
186
  if (isTerminalOracleJob(job)) {
178
- emitCommandOutput(ctx, `Job is already terminal: ${job.status}. Use /oracle-read ${job.id} for details or /oracle-clean ${job.id} to remove it.`, "info");
187
+ emitCommandOutput(pi, ctx, `Job is already terminal: ${job.status}. Use /oracle-read ${job.id} for details or /oracle-clean ${job.id} to remove it.`, "info");
179
188
  } else {
180
- emitCommandOutput(ctx, `Oracle job ${jobId} is not cancellable (${job.status})`, "info");
189
+ emitCommandOutput(pi, ctx, `Oracle job ${jobId} is not cancellable (${job.status})`, "info");
181
190
  }
182
191
  return;
183
192
  }
@@ -187,7 +196,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
187
196
  await promoteQueuedJobs({ workerPath, source: "oracle_cancel_command" });
188
197
  }
189
198
  refreshOracleStatus(ctx);
190
- emitCommandOutput(ctx, formatOracleCancelOutcome(cancelled), "info");
199
+ emitCommandOutput(pi, ctx, formatOracleCancelOutcome(cancelled), "info");
191
200
  },
192
201
  });
193
202
 
@@ -196,19 +205,20 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
196
205
  handler: async (args, ctx: ExtensionCommandContext) => {
197
206
  const target = args.trim();
198
207
  if (!target) {
199
- emitCommandOutput(ctx, "Usage: /oracle-clean <job-id|all>", "warning");
208
+ emitCommandOutput(pi, ctx, "Usage: /oracle-clean <job-id|all>", "warning");
200
209
  return;
201
210
  }
202
211
 
203
212
  const jobs = target === "all" ? listJobsForCwd(ctx.cwd) : [readScopedJob(target, ctx.cwd)].filter(Boolean);
204
213
  if (jobs.length === 0) {
205
- emitCommandOutput(ctx, "No matching oracle jobs found", "warning");
214
+ emitCommandOutput(pi, ctx, "No matching oracle jobs found", "warning");
206
215
  return;
207
216
  }
208
217
 
209
218
  const nonTerminalJobs = jobs.filter((job): job is NonNullable<typeof job> => Boolean(job && !isTerminalOracleJob(job)));
210
219
  if (nonTerminalJobs.length > 0) {
211
220
  emitCommandOutput(
221
+ pi,
212
222
  ctx,
213
223
  `Refusing to remove non-terminal oracle job${nonTerminalJobs.length === 1 ? "" : "s"}: ${nonTerminalJobs.map((job) => job.id).join(", ")}`,
214
224
  "warning",
@@ -246,7 +256,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
246
256
  : removedCount === jobs.length
247
257
  ? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
248
258
  : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} due to cleanup blockers or warnings.`;
249
- emitCommandOutput(ctx, `${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
259
+ emitCommandOutput(pi, ctx, `${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
250
260
  },
251
261
  });
252
262
  }
@@ -308,7 +308,7 @@ function detectDefaultChromeUserAgent(executablePath: string | undefined): strin
308
308
  const platformToken = chromeUserAgentPlatformToken(process.platform);
309
309
  if (!platformToken) return undefined;
310
310
  try {
311
- const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
311
+ const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv(), timeout: 1000 }).trim();
312
312
  const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+\.\d+)/);
313
313
  if (!versionMatch) return undefined;
314
314
  return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
@@ -318,8 +318,17 @@ function detectDefaultChromeUserAgent(executablePath: string | undefined): strin
318
318
  }
319
319
 
320
320
  const detectedChromeExecutablePath = detectDefaultChromeExecutablePath();
321
- const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
321
+ let detectedChromeUserAgent: string | undefined;
322
+ let detectedChromeUserAgentResolved = false;
322
323
  const agentExtensionsDir = join(getAgentDir(), "extensions");
324
+
325
+ function getDetectedChromeUserAgent(): string | undefined {
326
+ if (!detectedChromeUserAgentResolved) {
327
+ detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
328
+ detectedChromeUserAgentResolved = true;
329
+ }
330
+ return detectedChromeUserAgent;
331
+ }
323
332
  const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
324
333
 
325
334
  export interface OracleConfigLoadOptions {
@@ -442,7 +451,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
442
451
  authUrl: "https://chatgpt.com/auth/login",
443
452
  runMode: "headless",
444
453
  executablePath: detectedChromeExecutablePath,
445
- userAgent: detectedChromeUserAgent,
454
+ userAgent: undefined,
446
455
  args: ["--disable-blink-features=AutomationControlled"],
447
456
  },
448
457
  auth: {
@@ -732,5 +741,14 @@ export function loadOracleConfig(cwd: string, options?: OracleConfigLoadOptions)
732
741
  const details = getOracleConfigLoadDetails(cwd, options);
733
742
  const globalConfig = readJson(details.agentConfigPath);
734
743
  const projectConfig = details.projectConfigLoaded ? filterProjectConfig(readJson(details.projectConfigPath)) : undefined;
735
- return validateOracleConfig(deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig));
744
+ const globalBrowser = isObject(globalConfig) ? globalConfig.browser : undefined;
745
+ const hasConfiguredUserAgent = isObject(globalBrowser) && globalBrowser.userAgent !== undefined;
746
+ const defaults = {
747
+ ...DEFAULT_CONFIG,
748
+ browser: {
749
+ ...DEFAULT_CONFIG.browser,
750
+ userAgent: hasConfiguredUserAgent ? undefined : getDetectedChromeUserAgent(),
751
+ },
752
+ };
753
+ return validateOracleConfig(deepMerge(deepMerge(defaults, globalConfig), projectConfig));
736
754
  }
@@ -10,13 +10,14 @@ import { join } from "node:path";
10
10
  import { createArchive, type ArchiveCreationResult, type ArchiveSizeBreakdownRow } from "./archive.js";
11
11
  import { runOracleAuthBootstrap } from "./auth.js";
12
12
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
- import { Type } from "typebox";
13
+ import { Type, type Static } from "typebox";
14
14
  import { formatOracleCancelOutcome, formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
15
15
  import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
16
16
  import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
17
17
  import {
18
18
  coerceOracleSubmitPresetId,
19
19
  loadOracleConfig,
20
+ GROK_MODES,
20
21
  ORACLE_PROVIDERS,
21
22
  ORACLE_SUBMIT_PRESET_IDS,
22
23
  resolveOracleConfigForProvider,
@@ -64,6 +65,16 @@ import {
64
65
  tryAcquireRuntimeLease,
65
66
  } from "./runtime.js";
66
67
 
68
+ const ORACLE_PROVIDER_PARAM_DESCRIPTION = `Oracle web provider. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.`;
69
+ const ORACLE_PROVIDER_PARAM = Type.Optional(Type.String({
70
+ description: ORACLE_PROVIDER_PARAM_DESCRIPTION,
71
+ enum: [...ORACLE_PROVIDERS],
72
+ }));
73
+ const ORACLE_GROK_MODE_PARAM = Type.Optional(Type.String({
74
+ description: "Provider mode. For Grok, only heavy is currently supported. Omit to use the configured default mode.",
75
+ enum: [...GROK_MODES],
76
+ }));
77
+
67
78
  const ORACLE_SUBMIT_PARAMS = Type.Object({
68
79
  prompt: Type.String({ description: "Prompt text to send to ChatGPT or Grok web." }),
69
80
  files: Type.Array(Type.String({
@@ -74,11 +85,7 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
74
85
  description: "Exact project-relative files/directories to include in the oracle archive.",
75
86
  minItems: 1,
76
87
  }),
77
- provider: Type.Optional(
78
- Type.String({
79
- description: `Oracle web provider. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}. Use grok when the user asks to oracle to Grok. Grok archives are capped at 200 MiB.`,
80
- }),
81
- ),
88
+ provider: ORACLE_PROVIDER_PARAM,
82
89
  preset: Type.Optional(
83
90
  Type.String({
84
91
  description:
@@ -86,40 +93,36 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
86
93
  "Matching human-readable preset labels and common hyphen/space variants are normalized automatically. Do not pass preset when provider is grok.",
87
94
  }),
88
95
  ),
89
- mode: Type.Optional(
90
- Type.String({
91
- description: "Provider mode. For Grok, only heavy is currently supported. Omit to use the configured default mode.",
92
- }),
93
- ),
96
+ mode: ORACLE_GROK_MODE_PARAM,
94
97
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
95
98
  chatGptConversationId: Type.Optional(Type.String({
96
99
  description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, to continue. Omit for default behavior: starting a fresh oracle thread. Do not combine with followUpJobId.",
97
100
  minLength: 1,
98
101
  pattern: "^.*\\S.*$",
99
102
  })),
100
- });
103
+ }, { additionalProperties: false });
101
104
 
102
105
  const ORACLE_PREFLIGHT_PARAMS = Type.Object({
103
- provider: Type.Optional(Type.String({ description: `Provider readiness to check. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.` })),
106
+ provider: ORACLE_PROVIDER_PARAM,
104
107
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose provider/thread readiness should be checked." })),
105
108
  chatGptConversationId: Type.Optional(Type.String({
106
109
  description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, whose provider/thread readiness should be checked. Do not combine with followUpJobId.",
107
110
  minLength: 1,
108
111
  pattern: "^.*\\S.*$",
109
112
  })),
110
- });
113
+ }, { additionalProperties: false });
111
114
 
112
115
  const ORACLE_AUTH_PARAMS = Type.Object({
113
- provider: Type.Optional(Type.String({ description: `Provider auth seed to refresh. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.` })),
114
- });
116
+ provider: ORACLE_PROVIDER_PARAM,
117
+ }, { additionalProperties: false });
115
118
 
116
119
  const ORACLE_READ_PARAMS = Type.Object({
117
120
  jobId: Type.String({ description: "Oracle job id." }),
118
- });
121
+ }, { additionalProperties: false });
119
122
 
120
123
  const ORACLE_CANCEL_PARAMS = Type.Object({
121
124
  jobId: Type.String({ description: "Oracle job id." }),
122
- });
125
+ }, { additionalProperties: false });
123
126
 
124
127
  const MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME = 1;
125
128
  const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = resolveOracleProviderArchivePlan("chatgpt").maxArchiveBytes;
@@ -132,6 +135,24 @@ function normalizeOracleProvider(value: unknown, fallback: OracleProvider, toolN
132
135
  throw new Error(`Unknown ${toolName} provider: ${value}. Use chatgpt or grok.`);
133
136
  }
134
137
 
138
+ function prepareOracleProviderAliases<T>(args: unknown, toolName: string): T {
139
+ const record = asRecord(args);
140
+ if (!record) return args as T;
141
+ return {
142
+ ...record,
143
+ provider: typeof record.provider === "string" ? normalizeOracleProvider(record.provider, "chatgpt", toolName) : record.provider,
144
+ } as T;
145
+ }
146
+
147
+ function prepareOracleSubmitArguments(args: unknown): Static<typeof ORACLE_SUBMIT_PARAMS> {
148
+ const record = asRecord(args);
149
+ if (!record) return args as Static<typeof ORACLE_SUBMIT_PARAMS>;
150
+ return {
151
+ ...prepareOracleProviderAliases<Record<string, unknown>>(record, "oracle_submit"),
152
+ mode: typeof record.mode === "string" ? normalizeGrokMode(record.mode, "heavy") : record.mode,
153
+ } as Static<typeof ORACLE_SUBMIT_PARAMS>;
154
+ }
155
+
135
156
  function normalizeGrokMode(value: unknown, fallback: "heavy"): "heavy" {
136
157
  if (value === undefined) return fallback;
137
158
  if (typeof value !== "string") throw new Error("oracle_submit mode must be a string");
@@ -789,6 +810,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
789
810
  "Call oracle_preflight before doing expensive /oracle preparation. Pass provider='grok' when the user explicitly asks for Grok, followUpJobId for same-thread follow-ups from oracle-created jobs, or chatGptConversationId when the user explicitly provides an existing ChatGPT browser conversation id/URL. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
790
811
  ],
791
812
  parameters: ORACLE_PREFLIGHT_PARAMS,
813
+ prepareArguments: (args) => prepareOracleProviderAliases(args, "oracle_preflight"),
792
814
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
793
815
  const details = await runOraclePreflight(ctx, params);
794
816
  return {
@@ -809,6 +831,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
809
831
  "If oracle_auth itself fails, stop and report the failure instead of looping.",
810
832
  ],
811
833
  parameters: ORACLE_AUTH_PARAMS,
834
+ prepareArguments: (args) => prepareOracleProviderAliases(args, "oracle_auth"),
812
835
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
813
836
  try {
814
837
  const projectCwd = getProjectId(ctx.cwd);
@@ -838,26 +861,16 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
838
861
  "Optional thread target: pass `chatGptConversationId` only when the user explicitly provides an existing ChatGPT browser conversation id/URL to continue; omit it for the default fresh oracle thread.",
839
862
  promptSnippet: "Dispatch a background ChatGPT or Grok web oracle job after gathering repo context.",
840
863
  promptGuidelines: [
841
- "Gather context before calling oracle_submit.",
842
- "If the user explicitly provides an existing ChatGPT browser conversation id or https://chatgpt.com/c/... URL, pass it as chatGptConversationId and force provider='chatgpt'; otherwise omit chatGptConversationId so oracle_submit starts a fresh thread by default.",
843
- "If the immediately preceding oracle run failed because ChatGPT or Grok login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission; pass provider='grok' for Grok retries. Do not loop auth refreshes.",
844
- "Prefer context-rich archives up to the provider ceiling because more relevant surrounding context is usually better than less: 250 MB for ChatGPT and 200 MiB for Grok.",
845
- "By default, archive the whole repo by passing '.' for broad or unclear requests; 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.",
846
- "For narrower asks, still include nearby tests, docs, configs, and adjacent modules when they may improve answer quality. Only narrow aggressively when the user explicitly asks, privacy/sensitivity requires it, or size pressure forces it.",
847
- "Do not default to a one-file archive for a single function, file, or stack trace if the relevant surrounding context still fits comfortably within the limit.",
848
- "When files='.' and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like build/, dist/, out/, coverage/, and tmp/ outside obvious source roots like src/ and lib/ until the archive fits or no candidate remains; successful submissions report what was pruned.",
849
- "If a submitted oracle job later fails because upload is rejected, retry smaller: remove the largest obviously irrelevant/generated content first, then narrow to modified files plus adjacent files plus directly relevant subtrees, then explain the cut or ask the user if still needed.",
850
- "If oracle_submit fails before dispatch with details.error.code === 'archive_too_large' or an upload-limit message, treat that as retryable and retry automatically with a smaller archive.",
851
- "For archive_too_large retries, use the reported top-level size summary and any auto-pruned paths to decide what to cut: first remove the largest obviously irrelevant/generated/history/export content, then if needed narrow to the directly relevant subtrees plus adjacent docs/tests/config.",
852
- "Do not loop forever: stop after at most two total oracle_submit attempts for the same request, and if it still does not fit explain what was cut and why.",
853
- "For any other submit-time error, stop and report the error instead of retrying automatically.",
854
- "If oracle_submit returns a queued job instead of an immediately dispatched one, treat that as success and stop exactly the same way.",
855
- "After a successful or queued oracle_submit, stop; do not continue the task while the oracle job is running. If oracle_submit failed with retryable archive_too_large, narrow the archive and retry first.",
856
- "For ChatGPT, use `preset` as the only model-selection parameter on oracle_submit. " +
857
- `Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
858
- "matching human-readable preset labels are normalized automatically. Omit preset to use the configured default. For Grok, pass provider='grok' and omit preset; only Heavy is supported today.",
864
+ "Call oracle_preflight first, then gather enough context to choose archive inputs before oracle_submit.",
865
+ "Use context-rich archives when they fit; use files='.' for broad repo-wide asks. Default exclusions already skip common bulky outputs and obvious credentials/private data.",
866
+ "Pass chatGptConversationId only when the user explicitly provides an existing ChatGPT conversation id/URL; otherwise omit it for a fresh thread.",
867
+ "If auth is stale, call oracle_auth at most once before retrying. If oracle_auth fails, stop and report it.",
868
+ "If details.error.code is 'archive_too_large', retry once with a smaller archive using the reported size/pruning details; do not loop.",
869
+ "After a successful or queued oracle_submit, stop and report only the dispatch summary. Do not keep working while the oracle job runs.",
870
+ "For ChatGPT, use preset only when the user requests model control; otherwise omit it. For Grok, pass provider='grok' and omit preset.",
859
871
  ],
860
872
  parameters: ORACLE_SUBMIT_PARAMS,
873
+ prepareArguments: prepareOracleSubmitArguments,
861
874
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
862
875
  try {
863
876
  const projectCwd = getProjectId(ctx.cwd);
@@ -878,6 +891,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
878
891
  if (provider === "grok" && typeof params.preset === "string") {
879
892
  throw new Error("oracle_submit preset is only valid for ChatGPT. For Grok, use provider='grok' and mode='heavy'.");
880
893
  }
894
+ if (provider === "chatgpt" && typeof params.mode === "string") {
895
+ throw new Error("oracle_submit mode is only valid for Grok. For ChatGPT, omit mode and use preset for model selection.");
896
+ }
881
897
  const selection = provider === "grok"
882
898
  ? resolveOracleGrokMode(normalizeGrokMode(params.mode, baseConfig.defaults.grokMode))
883
899
  : resolveOracleSubmitPreset(typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : baseConfig.defaults.preset);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
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",
@@ -42,6 +42,9 @@
42
42
  "pi": {
43
43
  "extensions": [
44
44
  "./extensions/oracle/index.ts"
45
+ ],
46
+ "prompts": [
47
+ "./prompts"
45
48
  ]
46
49
  },
47
50
  "scripts": {
@@ -83,8 +86,8 @@
83
86
  "protobufjs": "7.6.1"
84
87
  },
85
88
  "devDependencies": {
86
- "@earendil-works/pi-ai": "^0.79.10",
87
- "@earendil-works/pi-coding-agent": "^0.79.10",
89
+ "@earendil-works/pi-ai": "^0.80.2",
90
+ "@earendil-works/pi-coding-agent": "^0.80.2",
88
91
  "@types/node": "^22.19.19",
89
92
  "esbuild": "^0.28.0",
90
93
  "tsx": "^4.22.3",
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Continue an earlier oracle job in the same provider thread
3
+ argument-hint: <job-id> <request>
3
4
  ---
4
5
  You are preparing an `/oracle-followup` job.
5
6
 
package/prompts/oracle.md CHANGED
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Prepare and dispatch a ChatGPT or Grok web oracle job
3
+ argument-hint: <request>
3
4
  ---
4
5
  You are preparing an /oracle job.
5
6
 
@@ -4,5 +4,5 @@ USER root
4
4
  RUN apt-get update \
5
5
  && apt-get install -y --no-install-recommends zstd rsync \
6
6
  && rm -rf /var/lib/apt/lists/*
7
- RUN npm install -g agent-browser@0.27.0
7
+ RUN npm install -g agent-browser@0.29.1
8
8
  USER circleci