pi-oracle 0.5.0 → 0.6.1

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.
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { spawn } from "node:child_process";
8
8
  import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
9
9
  import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
10
- import { dirname, join } from "node:path";
10
+ import { delimiter, dirname, join } from "node:path";
11
11
  import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
12
12
  import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
13
13
  import type { OracleConfig } from "./config.js";
@@ -21,6 +21,14 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
21
21
  ) || "agent-browser";
22
22
  const PROFILE_CLONE_TIMEOUT_MS = 120_000;
23
23
  const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
24
+ const WORKSPACE_ROOT_MARKERS = [
25
+ ".pi/extensions/oracle.json",
26
+ ] as const;
27
+ const REQUIRED_ORACLE_DEPENDENCIES = [
28
+ { name: "agent-browser", command: AGENT_BROWSER_BIN },
29
+ { name: "tar", command: "tar" },
30
+ { name: "zstd", command: "zstd" },
31
+ ] as const;
24
32
 
25
33
  export interface OracleRuntimeLeaseMetadata {
26
34
  jobId: string;
@@ -51,7 +59,7 @@ export interface OracleConversationLeaseAttempt {
51
59
  blocker?: OracleConversationLeaseMetadata;
52
60
  }
53
61
 
54
- export function getProjectId(cwd: string): string {
62
+ function resolveRealCwd(cwd: string): string {
55
63
  try {
56
64
  return realpathSync(cwd);
57
65
  } catch {
@@ -59,6 +67,26 @@ export function getProjectId(cwd: string): string {
59
67
  }
60
68
  }
61
69
 
70
+ function hasWorkspaceRootMarker(path: string): boolean {
71
+ return WORKSPACE_ROOT_MARKERS.some((marker) => existsSync(join(path, marker)));
72
+ }
73
+
74
+ function resolveWorkspaceRoot(realCwd: string): string {
75
+ let current = realCwd;
76
+ let nearestMarkerRoot: string | undefined;
77
+ while (true) {
78
+ if (existsSync(join(current, ".git"))) return current;
79
+ if (!nearestMarkerRoot && hasWorkspaceRootMarker(current)) nearestMarkerRoot = current;
80
+ const parent = dirname(current);
81
+ if (parent === current) return nearestMarkerRoot ?? realCwd;
82
+ current = parent;
83
+ }
84
+ }
85
+
86
+ export function getProjectId(cwd: string): string {
87
+ return resolveWorkspaceRoot(resolveRealCwd(cwd));
88
+ }
89
+
62
90
  export function hasPersistedSessionFile(originSessionFile: string | undefined): originSessionFile is string {
63
91
  return Boolean(originSessionFile);
64
92
  }
@@ -110,6 +138,99 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
110
138
  return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
111
139
  }
112
140
 
141
+ function missingBrowserExecutableMessage(executablePath: string): string {
142
+ return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
143
+ }
144
+
145
+ function nonExecutableBrowserMessage(executablePath: string): string {
146
+ return `Configured oracle browser executable is not executable: ${executablePath}. Fix browser.executablePath permissions or point it at a runnable Chrome binary.`;
147
+ }
148
+
149
+ function missingLocalDependencyMessage(name: string): string {
150
+ return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
151
+ }
152
+
153
+ function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
154
+ return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
155
+ }
156
+
157
+ async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
158
+ if (!command) return undefined;
159
+ if (command.includes("/")) {
160
+ try {
161
+ await access(command, fsConstants.X_OK);
162
+ return command;
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ }
167
+
168
+ const pathValue = process.env.PATH ?? "";
169
+ for (const segment of pathValue.split(delimiter)) {
170
+ if (!segment) continue;
171
+ const candidate = join(segment, command);
172
+ try {
173
+ await access(candidate, fsConstants.X_OK);
174
+ return candidate;
175
+ } catch {
176
+ continue;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ async function assertConfiguredBrowserExecutableReady(executablePath: string | undefined): Promise<void> {
183
+ if (!executablePath) return;
184
+ let executableStats;
185
+ try {
186
+ executableStats = await stat(executablePath);
187
+ } catch (error) {
188
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
189
+ if (code === "ENOENT") throw new Error(missingBrowserExecutableMessage(executablePath));
190
+ if (code === "EACCES" || code === "EPERM") throw new Error(nonExecutableBrowserMessage(executablePath));
191
+ throw new Error(`Failed to inspect configured oracle browser executable ${executablePath}: ${error instanceof Error ? error.message : String(error)}`);
192
+ }
193
+
194
+ if (!executableStats.isFile()) {
195
+ throw new Error(nonExecutableBrowserMessage(executablePath));
196
+ }
197
+
198
+ try {
199
+ await access(executablePath, fsConstants.X_OK);
200
+ } catch {
201
+ throw new Error(nonExecutableBrowserMessage(executablePath));
202
+ }
203
+ }
204
+
205
+ async function assertRequiredLocalDependencyReady(name: string, command: string): Promise<void> {
206
+ const resolved = await resolveExecutableOnPath(command);
207
+ if (!resolved) throw new Error(missingLocalDependencyMessage(name));
208
+ }
209
+
210
+ async function assertWritableDirectory(path: string, label: "runtime profiles" | "jobs"): Promise<void> {
211
+ try {
212
+ await mkdir(path, { recursive: true, mode: 0o700 });
213
+ } catch {
214
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
215
+ }
216
+
217
+ let directoryStats;
218
+ try {
219
+ directoryStats = await stat(path);
220
+ } catch {
221
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
222
+ }
223
+ if (!directoryStats.isDirectory()) {
224
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
225
+ }
226
+
227
+ try {
228
+ await access(path, fsConstants.W_OK | fsConstants.X_OK);
229
+ } catch {
230
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
231
+ }
232
+ }
233
+
113
234
  export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
114
235
  const seedDir = config.browser.authSeedProfileDir;
115
236
  let seedStats;
@@ -135,6 +256,12 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
135
256
 
136
257
  export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
137
258
  await assertOracleAuthSeedProfileReady(config);
259
+ await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
260
+ for (const dependency of REQUIRED_ORACLE_DEPENDENCIES) {
261
+ await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
262
+ }
263
+ await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
264
+ await assertWritableDirectory(ORACLE_JOBS_DIR, "jobs");
138
265
  }
139
266
 
140
267
  export function getSeedGeneration(config: OracleConfig): string | undefined {
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { tmpdir } from "node:os";
9
9
  import { basename, join, posix } from "node:path";
10
+ import { runOracleAuthBootstrap } from "./auth.js";
10
11
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
11
12
  import { Type } from "@sinclair/typebox";
12
13
  import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
@@ -58,7 +59,11 @@ import {
58
59
 
59
60
  const ORACLE_SUBMIT_PARAMS = Type.Object({
60
61
  prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
61
- files: Type.Array(Type.String({ description: "Project-relative file or directory path to include in the archive." }), {
62
+ files: Type.Array(Type.String({
63
+ description: "Project-relative file or directory path to include in the archive.",
64
+ minLength: 1,
65
+ pattern: ".*\\S.*",
66
+ }), {
62
67
  description: "Exact project-relative files/directories to include in the oracle archive.",
63
68
  minItems: 1,
64
69
  }),
@@ -150,6 +155,20 @@ type ArchiveCreationResult = {
150
155
  includedEntries: string[];
151
156
  };
152
157
 
158
+ function appendArchiveEntries(target: string[], source: Iterable<string>): void {
159
+ for (const entry of source) target.push(entry);
160
+ }
161
+
162
+ function mergeArchiveEntryGroups(groups: Iterable<Iterable<string>>): string[] {
163
+ const merged: string[] = [];
164
+ for (const group of groups) appendArchiveEntries(merged, group);
165
+ return merged;
166
+ }
167
+
168
+ export function mergeArchiveEntryGroupsForTesting(groups: Iterable<Iterable<string>>): string[] {
169
+ return mergeArchiveEntryGroups(groups);
170
+ }
171
+
153
172
  function pathContainsSequence(relativePath: string, sequence: readonly string[]): boolean {
154
173
  const segments = relativePath.split("/").filter(Boolean);
155
174
  if (sequence.length === 0 || segments.length < sequence.length) return false;
@@ -263,7 +282,7 @@ async function expandArchiveEntries(cwd: string, relativePath: string, options?:
263
282
  for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
264
283
  const childRelative = child.name;
265
284
  if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child)) continue;
266
- if (child.isDirectory()) results.push(...await expandArchiveEntries(cwd, childRelative));
285
+ if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative));
267
286
  else results.push(childRelative);
268
287
  }
269
288
  return results;
@@ -279,7 +298,7 @@ async function expandArchiveEntries(cwd: string, relativePath: string, options?:
279
298
  for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
280
299
  const childRelative = posix.join(normalized, child.name);
281
300
  if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child, { forceInclude: options?.forceIncludeSubtree })) continue;
282
- if (child.isDirectory()) results.push(...await expandArchiveEntries(cwd, childRelative, { forceIncludeSubtree: options?.forceIncludeSubtree }));
301
+ if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative, { forceIncludeSubtree: options?.forceIncludeSubtree }));
283
302
  else results.push(childRelative);
284
303
  }
285
304
  return results;
@@ -289,11 +308,12 @@ async function resolveExpandedArchiveEntriesFromInputs(
289
308
  cwd: string,
290
309
  entries: Array<{ absolute: string; relative: string }>,
291
310
  ): Promise<string[]> {
292
- return Array.from(new Set((await Promise.all(entries.map(async (entry) => {
311
+ const expandedGroups = await Promise.all(entries.map(async (entry) => {
293
312
  const statEntry = await lstat(entry.absolute);
294
313
  const forceIncludeSubtree = statEntry.isDirectory() && entry.relative !== "." && shouldExcludeArchivePath(entry.relative, true);
295
314
  return expandArchiveEntries(cwd, entry.relative, { forceIncludeSubtree });
296
- }))).flat())).sort();
315
+ }));
316
+ return Array.from(new Set(mergeArchiveEntryGroups(expandedGroups))).sort();
297
317
  }
298
318
 
299
319
  export async function resolveExpandedArchiveEntries(cwd: string, files: string[]): Promise<string[]> {
@@ -568,7 +588,7 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
568
588
  };
569
589
  }
570
590
 
571
- type OracleToolName = "oracle_submit" | "oracle_read" | "oracle_cancel";
591
+ type OracleToolName = "oracle_auth" | "oracle_submit" | "oracle_read" | "oracle_cancel";
572
592
  type OracleToolErrorSource = OracleToolName | "oracle_preflight";
573
593
  type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
574
594
  type OracleToolErrorDetails = {
@@ -587,7 +607,7 @@ type OracleToolJobDetailsOptions = {
587
607
  responseAvailable?: boolean;
588
608
  };
589
609
 
590
- const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_submit", "oracle_read", "oracle_cancel"]);
610
+ const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_auth", "oracle_submit", "oracle_read", "oracle_cancel"]);
591
611
 
592
612
  function asRecord(value: unknown): Record<string, unknown> | undefined {
593
613
  return typeof value === "object" && value !== null && !Array.isArray(value)
@@ -682,7 +702,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
682
702
  code: "auth_seed_profile_missing",
683
703
  message,
684
704
  rejectedValue: message.replace(/^Oracle auth seed profile not found: /, "").replace(/\. Run \/oracle-auth first\.$/, ""),
685
- suggestedNextStep: "Run /oracle-auth first, then retry the oracle tool call.",
705
+ suggestedNextStep: "Call oracle_auth or run /oracle-auth once, then retry the oracle tool call.",
686
706
  };
687
707
  }
688
708
 
@@ -691,7 +711,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
691
711
  code: "auth_seed_profile_unreadable",
692
712
  message,
693
713
  rejectedValue: message.replace(/^Oracle auth seed profile is not readable: /, "").replace(/\. Fix its permissions or rerun \/oracle-auth\.$/, ""),
694
- suggestedNextStep: "Fix the auth seed profile permissions or rerun /oracle-auth, then retry.",
714
+ suggestedNextStep: "Fix the auth seed profile permissions or call oracle_auth / rerun /oracle-auth once, then retry.",
695
715
  };
696
716
  }
697
717
 
@@ -700,7 +720,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
700
720
  code: "auth_seed_profile_invalid_type",
701
721
  message,
702
722
  rejectedValue: message.replace(/^Oracle auth seed profile is not a directory: /, "").replace(/\. Remove the invalid path or rerun \/oracle-auth\.$/, ""),
703
- suggestedNextStep: "Remove the invalid auth seed path or rerun /oracle-auth to recreate it.",
723
+ suggestedNextStep: "Remove the invalid auth seed path or call oracle_auth / rerun /oracle-auth once to recreate it.",
704
724
  };
705
725
  }
706
726
 
@@ -712,6 +732,52 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
712
732
  };
713
733
  }
714
734
 
735
+ if (message.startsWith("Configured oracle browser executable does not exist: ")) {
736
+ return {
737
+ code: "browser_executable_missing",
738
+ message,
739
+ rejectedValue: message.replace(/^Configured oracle browser executable does not exist: /, "").replace(/\. Fix browser\.executablePath or install Chrome there\.$/, ""),
740
+ suggestedNextStep: "Fix browser.executablePath or install Chrome at that path, then retry.",
741
+ };
742
+ }
743
+
744
+ if (message.startsWith("Configured oracle browser executable is not executable: ")) {
745
+ return {
746
+ code: "browser_executable_not_executable",
747
+ message,
748
+ rejectedValue: message.replace(/^Configured oracle browser executable is not executable: /, "").replace(/\. Fix browser\.executablePath permissions or point it at a runnable Chrome binary\.$/, ""),
749
+ suggestedNextStep: "Fix browser.executablePath permissions or point it at a runnable Chrome binary, then retry.",
750
+ };
751
+ }
752
+
753
+ if (message.startsWith("Oracle prerequisite not found on PATH: ")) {
754
+ const rejectedValue = message.replace(/^Oracle prerequisite not found on PATH: /, "").replace(/\. Install .*$/, "");
755
+ return {
756
+ code: "local_dependency_missing",
757
+ message,
758
+ rejectedValue,
759
+ suggestedNextStep: `Install ${rejectedValue || "the missing dependency"} and retry.`,
760
+ };
761
+ }
762
+
763
+ if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
764
+ return {
765
+ code: "runtime_profiles_dir_unwritable",
766
+ message,
767
+ rejectedValue: message.replace(/^Oracle runtime profiles directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
768
+ suggestedNextStep: "Fix browser.runtimeProfilesDir permissions or configure a writable directory, then retry.",
769
+ };
770
+ }
771
+
772
+ if (message.startsWith("Oracle jobs directory is not writable: ")) {
773
+ return {
774
+ code: "jobs_dir_unwritable",
775
+ message,
776
+ rejectedValue: message.replace(/^Oracle jobs directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
777
+ suggestedNextStep: "Fix PI_ORACLE_JOBS_DIR permissions or point it at a writable directory, then retry.",
778
+ };
779
+ }
780
+
715
781
  if (toolName === "oracle_submit" && message === "oracle_submit requires at least one file or directory to archive") {
716
782
  return {
717
783
  code: "archive_input_required",
@@ -720,6 +786,22 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
720
786
  };
721
787
  }
722
788
 
789
+ if (toolName === "oracle_submit" && message === "Archive input must be a non-empty project-relative path") {
790
+ return {
791
+ code: "archive_input_blank",
792
+ message,
793
+ suggestedNextStep: "Retry with a non-empty project-relative file or directory path. Use '.' only when you intentionally want a whole-repo archive.",
794
+ };
795
+ }
796
+
797
+ if (toolName === "oracle_submit" && message === "Archive input must use '.' exactly for a whole-repo archive") {
798
+ return {
799
+ code: "archive_input_whole_repo_sentinel_invalid",
800
+ message,
801
+ suggestedNextStep: "If you want a whole-repo archive, pass '.' exactly. Otherwise pass an exact project-relative path without extra padding.",
802
+ };
803
+ }
804
+
723
805
  if (toolName === "oracle_submit" && message.startsWith("Archive input does not exist: ")) {
724
806
  return {
725
807
  code: "archive_input_missing",
@@ -815,7 +897,13 @@ function buildOracleToolErrorResult(
815
897
  ) {
816
898
  const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
817
899
  return {
818
- content: [{ type: "text" as const, text: errorDetails.message }],
900
+ content: [{
901
+ type: "text" as const,
902
+ text: [
903
+ errorDetails.message,
904
+ errorDetails.suggestedNextStep ? `Suggested next step: ${errorDetails.suggestedNextStep}` : undefined,
905
+ ].filter(Boolean).join("\n"),
906
+ }],
819
907
  details: {
820
908
  job: options?.job ? redactJobDetails(options.job, options.jobDetails) : undefined,
821
909
  error: errorDetails,
@@ -887,15 +975,16 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
887
975
  try {
888
976
  await assertOracleSubmitPrerequisites(config);
889
977
  } catch (error) {
978
+ const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
890
979
  return {
891
980
  ready: false,
892
981
  session: { persisted: true, sessionFile },
893
982
  config: { ready: true },
894
983
  auth: {
895
- ready: false,
984
+ ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
896
985
  seedProfileDir: config.browser.authSeedProfileDir,
897
986
  },
898
- error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
987
+ error: errorDetails,
899
988
  };
900
989
  }
901
990
 
@@ -910,7 +999,7 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
910
999
  };
911
1000
  }
912
1001
 
913
- export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void {
1002
+ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWorkerPath = workerPath): void {
914
1003
  pi.on("tool_result", async (event) => {
915
1004
  if (!ORACLE_TOOL_NAMES.has(event.toolName as OracleToolName)) return;
916
1005
  if (event.isError) return;
@@ -939,6 +1028,34 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
939
1028
  },
940
1029
  });
941
1030
 
1031
+ pi.registerTool({
1032
+ name: "oracle_auth",
1033
+ label: "Oracle Auth",
1034
+ description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured real Chrome profile.",
1035
+ promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
1036
+ promptGuidelines: [
1037
+ "Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
1038
+ "At most once per user request, refresh auth and then retry the blocked oracle submission.",
1039
+ "If oracle_auth itself fails, stop and report the failure instead of looping.",
1040
+ ],
1041
+ parameters: Type.Object({}),
1042
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1043
+ try {
1044
+ const projectCwd = getProjectId(ctx.cwd);
1045
+ const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd);
1046
+ return {
1047
+ content: [{ type: "text" as const, text: message }],
1048
+ details: {
1049
+ refreshed: true,
1050
+ authSeedProfileDir: loadOracleConfig(projectCwd).browser.authSeedProfileDir,
1051
+ },
1052
+ };
1053
+ } catch (error) {
1054
+ return buildOracleToolErrorResult("oracle_auth", error, {});
1055
+ }
1056
+ },
1057
+ });
1058
+
942
1059
  pi.registerTool({
943
1060
  name: "oracle_submit",
944
1061
  label: "Oracle Submit",
@@ -948,9 +1065,11 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
948
1065
  promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
949
1066
  promptGuidelines: [
950
1067
  "Gather context before calling oracle_submit.",
951
- "By default, archive the whole repo 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.",
952
- "Only narrow file selection when the user explicitly asks, the task is clearly scoped smaller, or privacy/sensitivity requires it.",
953
- "For very targeted asks like a single function or stack trace, a smaller archive is preferable.",
1068
+ "If the immediately preceding oracle run failed because ChatGPT login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission. Do not loop auth refreshes.",
1069
+ "Prefer context-rich archives up to the 250 MB ceiling because more relevant surrounding context is usually better than less.",
1070
+ "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.",
1071
+ "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.",
1072
+ "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.",
954
1073
  "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.",
955
1074
  "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.",
956
1075
  "If oracle_submit itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error instead of retrying automatically.",
@@ -961,18 +1080,19 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
961
1080
  parameters: ORACLE_SUBMIT_PARAMS,
962
1081
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
963
1082
  try {
964
- const config = loadOracleConfig(ctx.cwd);
1083
+ const projectCwd = getProjectId(ctx.cwd);
1084
+ const config = loadOracleConfig(projectCwd);
965
1085
  const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
966
- const projectId = getProjectId(ctx.cwd);
1086
+ const projectId = getProjectId(projectCwd);
967
1087
  const sessionId = getSessionId(originSessionFile, projectId);
968
1088
  const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
969
1089
  const selection = resolveOracleSubmitPreset(presetId);
970
- const followUp = resolveFollowUp(params.followUpJobId, ctx.cwd);
1090
+ const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
971
1091
  // Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
972
- resolveArchiveInputs(ctx.cwd, params.files);
1092
+ resolveArchiveInputs(projectCwd, params.files);
973
1093
  await assertOracleSubmitPrerequisites(config);
974
1094
  try {
975
- await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: ctx.cwd }, async () => {
1095
+ await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: projectCwd }, async () => {
976
1096
  await reconcileStaleOracleJobs();
977
1097
  await pruneTerminalOracleJobs();
978
1098
  });
@@ -993,7 +1113,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
993
1113
  let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
994
1114
 
995
1115
  try {
996
- archive = await createArchive(ctx.cwd, params.files, tempArchivePath);
1116
+ archive = await createArchive(projectCwd, params.files, tempArchivePath);
997
1117
  const currentArchive = archive;
998
1118
  await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
999
1119
  await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
@@ -1036,7 +1156,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
1036
1156
  chatUrl: followUp.chatUrl,
1037
1157
  requestSource: "tool",
1038
1158
  },
1039
- ctx.cwd,
1159
+ projectCwd,
1040
1160
  originSessionFile,
1041
1161
  config,
1042
1162
  runtime,
@@ -1079,7 +1199,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
1079
1199
  chatUrl: followUp.chatUrl,
1080
1200
  requestSource: "tool",
1081
1201
  },
1082
- ctx.cwd,
1202
+ projectCwd,
1083
1203
  originSessionFile,
1084
1204
  config,
1085
1205
  runtime,
@@ -111,11 +111,11 @@ export function buildOracleWakeupNotificationContent(job, options = {}) {
111
111
  const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
112
112
  return [
113
113
  `Oracle job ${job.id} is ${job.status}.`,
114
- `Use oracle_read with jobId ${job.id} to open the response and settle wake-up retries.`,
114
+ `Use /oracle-read ${job.id} to inspect the saved response preview. /oracle-status ${job.id} still shows saved job metadata. Agent callers can use oracle_read({ jobId: "${job.id}" }) if they need tool output in the current turn.`,
115
115
  responseLine,
116
116
  `Artifacts: ${artifactsPath}`,
117
117
  formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
118
- job.error ? `Error: ${job.error}` : "After oracle_read, continue from the oracle output.",
118
+ job.error ? `Error: ${job.error}` : "After opening the saved result, continue from the oracle output.",
119
119
  ].filter(Boolean).join("\n");
120
120
  }
121
121
 
@@ -69,7 +69,7 @@ export function classifyChatAuthPage(args) {
69
69
  const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
70
70
  const hasModelControl =
71
71
  args.snapshot.includes('button "Model selector"') ||
72
- /button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(args.snapshot);
72
+ /button "(?:Instant|(?:(?:Light|Standard|Extended|Heavy) )?Thinking|(?:(?:Light|Standard|Extended|Heavy) )?Pro)(?:, click to remove)?"/i.test(args.snapshot);
73
73
 
74
74
  const challengePatterns = [
75
75
  /just a moment/i,