pi-oracle 0.7.7 → 0.7.9

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,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.9 - 2026-06-11
6
+
7
+ ### Fixed
8
+ - made oracle workers launch their isolated Chrome runtime directly and attach `agent-browser` via DevTools, avoiding failures when unrelated `agent-browser` sessions or daemons are already running
9
+ - tightened worker-owned browser cleanup so runtime profiles are deleted only after the isolated Chrome process has been closed or terminated
10
+ - rejected `browser.args` overrides that would bypass oracle-managed Chrome profile or DevTools isolation
11
+
12
+ ### Validation
13
+ - verified ChatGPT and Grok oracle smoke tests against the local source extension after the worker-owned browser launch fix
14
+
15
+ ## 0.7.8 - 2026-06-11
16
+
17
+ ### Changed
18
+ - updated the local pi development baseline to `@earendil-works/pi-coding-agent` / `@earendil-works/pi-ai` `0.79.1` and regenerated the npm lockfile
19
+ - documented `pi` `0.79.1+` as the suggested tested floor while keeping pi runtime packages as optional wildcard peers so npm peer ranges do not block users from trying newer pi releases
20
+ - updated isolated local-extension and packed package validation workflows to pass explicit `--approve` when they intentionally trust their temporary project fixtures under Pi 0.79.1 project-trust rules
21
+ - made TUI `/oracle` and `/oracle-followup` commands reappear as compact user messages for prompt-history/up-arrow recall while keeping verbose dispatch instructions hidden
22
+
23
+ ### Fixed
24
+ - made project-local `.pi/extensions/oracle.json` overrides honor Pi's effective project trust decision (`ctx.isProjectTrusted()`), including `--no-approve` and saved “do not trust” decisions while preserving the historical default of loading safe project overrides for existing oracle users
25
+ - updated ChatGPT model-selection handling for the current compact selector labels, including `Extra High`, `Pro Standard`, and `Pro Extended`
26
+ - hardened ChatGPT/Grok send handling so oracle workers require provider acceptance evidence before entering `awaiting_response`, preventing unsent composer drafts from masquerading as running jobs
27
+ - dismissed ChatGPT Pro feedback dialogs during model configuration instead of mistaking their generic `Close` control for configuration UI
28
+
29
+ ### Compatibility
30
+ - reviewed the pi `0.79.1` changelog, project-trust docs, extension docs, package docs, prompt-template docs, SDK/RPC exports, and matching examples; the oracle extension remains compatible with current extension lifecycle and package install/update behavior
31
+
5
32
  ## 0.7.7 - 2026-06-08
6
33
 
7
34
  ### 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.0`. Pi `0.79.0+` 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. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.1`. Pi `0.79.1+` 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
 
@@ -77,7 +77,7 @@ You need:
77
77
 
78
78
  - macOS, Linux, or Windows native
79
79
  - Node.js 22 or newer
80
- - Suggested tested floor: `pi` 0.79.0 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
80
+ - Suggested tested floor: `pi` 0.79.1 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
81
81
  - Google Chrome/Chromium or another Chromium-family browser
82
82
  - ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
83
83
  - `agent-browser`, `tar`, and `zstd` available on the machine
@@ -149,7 +149,7 @@ flowchart LR
149
149
 
150
150
  Key design choices:
151
151
 
152
- - **Prompt templates own context gathering.** `/oracle` and `/oracle-followup` tell the agent how to preflight, gather context, choose archive inputs, and then stop after dispatch.
152
+ - **Extension-managed dispatch owns context gathering.** In the TUI, `/oracle` and `/oracle-followup` are intercepted before prompt-template expansion, re-added as compact user messages for prompt-history/up-arrow recall, and paired with detailed dispatch instructions as hidden context. The visible transcript stays compact while the agent still preflights, gathers context, chooses archive inputs, and stops after dispatch.
153
153
  - **Tools own execution.** `oracle_submit` builds the archive, admits or queues the job, starts the worker, and returns immediately.
154
154
  - **Auth uses a seed profile.** `/oracle-auth` imports cookies into an isolated seed profile; each job clones that seed into its own temporary runtime profile.
155
155
  - **Follow-ups preserve provider thread state.** `/oracle-followup <job-id> ...` resolves the prior job's saved provider URL and submits the next prompt with `followUpJobId`.
@@ -179,7 +179,7 @@ Agent-facing tools:
179
179
 
180
180
  Most users can start with defaults. Set an agent-level config only when you need a non-default provider, mode, preset, or browser profile.
181
181
 
182
- Pi 0.79.0 gates project-local inputs behind project trust. `pi-oracle` preserves its historical risk-on extension behavior for existing users: project-local `.pi/extensions/oracle.json` safe overrides still load by default for compatibility. They are ignored when you explicitly opt out of project-local inputs with `--no-approve` or save a “do not trust” decision for the project. Privileged browser/auth settings still come only from the agent-level config.
182
+ Pi 0.79.1 gates project-local inputs behind project trust. `pi-oracle` preserves its historical risk-on extension behavior for existing users: project-local `.pi/extensions/oracle.json` safe overrides still load by default for compatibility. They are ignored when you explicitly opt out of project-local inputs with `--no-approve` or save a “do not trust” decision for the project. Privileged browser/auth settings still come only from the agent-level config.
183
183
 
184
184
  `~/.pi/agent/extensions/oracle.json`
185
185
 
@@ -406,8 +406,8 @@ For manual end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_
406
406
  | `extensions/oracle/lib/` | Commands, tools, config, jobs, queueing, runtime, poller |
407
407
  | `extensions/oracle/worker/` | Detached provider web worker and UI/auth helpers |
408
408
  | `extensions/oracle/shared/` | Shared process, state, job, and observability helpers |
409
- | [`prompts/oracle.md`](prompts/oracle.md) | `/oracle` prompt-template workflow |
410
- | [`prompts/oracle-followup.md`](prompts/oracle-followup.md) | `/oracle-followup` prompt-template workflow |
409
+ | [`prompts/oracle.md`](prompts/oracle.md) | Hidden `/oracle` command-dispatch workflow |
410
+ | [`prompts/oracle-followup.md`](prompts/oracle-followup.md) | Hidden `/oracle-followup` command-dispatch workflow |
411
411
  | `scripts/oracle-sanity-*` | Local sanity and archive-safety checks |
412
412
  | `scripts/platform-smoke*` | Crabbox macOS, Ubuntu, and Windows release smoke gate |
413
413
  | [`docs/ORACLE_DESIGN.md`](docs/ORACLE_DESIGN.md) | Architecture, lifecycle, queueing, persistence, recovery behavior |
@@ -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.0+ is the suggested tested floor for current project-trust-aware package/runtime validation
10
+ - `pi` 0.79.1+ 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
 
@@ -60,14 +60,15 @@ The extension now follows the current `pi` session lifecycle model:
60
60
  - previous runtimes are expected to clean up in `session_shutdown`
61
61
  - no new logic depends on removed post-transition events
62
62
 
63
- ### Prompt template
63
+ ### Oracle dispatch commands
64
64
 
65
65
  - `/oracle <request>`
66
- - implemented as a prompt template, not an extension command
66
+ - in TUI mode, intercepted by the extension before prompt-template expansion so verbose internal workflow rules stay hidden from the visible transcript
67
+ - injects the detailed dispatch instructions as a hidden custom message
68
+ - in print/json/rpc modes, the extension contributes the prompt templates so non-interactive prompt expansion still works
67
69
  - asks the agent to gather context and dispatch an oracle job
68
- - intentionally uses native pi prompt/template queueing so submissions survive streaming and compaction
69
70
  - `/oracle-followup <job-id> <request>`
70
- - implemented as a prompt template, not an extension command
71
+ - follows the same hidden-instructions TUI path and print/json prompt-template fallback
71
72
  - asks the agent to continue an earlier oracle job in the same provider thread via `followUpJobId`
72
73
  - keeps same-thread continuation available to normal users without requiring raw tool-call syntax
73
74
 
@@ -106,14 +107,14 @@ The extension now follows the current `pi` session lifecycle model:
106
107
  ### `/oracle ...`
107
108
 
108
109
  `/oracle <request>` should not directly drive ChatGPT or Grok.
109
- It expands through the prompt-template path so pi can apply its native queueing semantics before the agent starts work.
110
+ In TUI mode, the extension intercepts it before prompt-template expansion, re-injects the compact slash request as the visible user message so prompt-history/up-arrow recall survives session reloads, injects hidden dispatch instructions before the agent starts, and shows only compact user-facing status. In print/json/rpc modes, the extension exposes the prompt template so one-shot `/oracle` still expands and runs normally.
110
111
 
111
- Instead it instructs the agent to:
112
+ It instructs the agent to:
112
113
 
113
114
  1. call `oracle_preflight` immediately, passing `provider: "grok"` when the user explicitly asks for Grok
114
115
  2. stop right away if preflight reports the session or local oracle setup is not ready
115
116
  3. understand whether the request is explicitly narrow or genuinely broad
116
- 4. 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
117
+ 4. if auth is missing, stale, or the worker explicitly said to rerun `/oracle-auth`, stop and tell the user to run `/oracle-auth` rather than launching auth automatically
117
118
  5. gather enough repo context to submit well and bias toward context-rich archives when they fit within the provider ceiling: 250 MB for ChatGPT and 200 MiB for Grok
118
119
  6. if the request is narrow, start from the directly relevant area but still include nearby tests, docs, config, and adjacent modules when they may improve answer quality
119
120
  7. if the request is broad/repo-wide, gather broader context and usually archive `.`
@@ -229,7 +230,7 @@ Merged config locations:
229
230
  - global: `~/.pi/agent/extensions/oracle.json`
230
231
  - project: `.pi/extensions/oracle.json`
231
232
 
232
- Project config remains restricted to safe overrides only. On Pi 0.79.0+, pi itself gates project-local inputs behind project trust, but `pi-oracle` keeps its historical risk-on extension behavior for this package-specific safe override file: `.pi/extensions/oracle.json` loads by default for compatibility, and is ignored only when the run passes `--no-approve` or the project has a saved “do not trust” decision. This preserves the existing extension experience while still honoring explicit opt-out/distrust decisions. Browser/auth settings remain global-only because they control local privileged browser state.
233
+ Project config remains restricted to safe overrides only. On Pi 0.79.1+, pi itself gates project-local inputs behind project trust, but `pi-oracle` keeps its historical risk-on extension behavior for this package-specific safe override file: `.pi/extensions/oracle.json` loads by default for compatibility, and is ignored when Pi reports the project is untrusted, including `--no-approve` or saved “do not trust” decisions. This preserves the existing extension experience while still honoring explicit opt-out/distrust decisions. Browser/auth settings remain global-only because they control local privileged browser state.
233
234
 
234
235
  ### Current config shape
235
236
 
@@ -631,6 +632,9 @@ Remaining non-blocking hardening work:
631
632
  - keep hardening model-selection verification against future ChatGPT UI variation
632
633
 
633
634
  Recent proof points:
635
+ - Pi 0.79.1 release gate: `npm run release:check` passed on 2026-06-11 after the project-trust, prompt-history, ChatGPT selector, and send-acceptance updates, including `verify:oracle` plus Crabbox macOS, Ubuntu, and Windows native `platform-build` and `real-extension` suites
636
+ - Pi 0.79.1 platform artifacts: `.artifacts/platform-smoke/run-1781196218405-311wzs` (macOS platform-build), `.artifacts/platform-smoke/run-1781196261807-eb0391` (macOS real-extension), `.artifacts/platform-smoke/run-1781196230636-ze1hai` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1781196265638-kxiwh9` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1781196255488-ucuf35` (Windows native platform-build), `.artifacts/platform-smoke/run-1781196369098-4qlzjs` (Windows native real-extension)
637
+ - Pi 0.79.1 live source-extension send-acceptance smoke: new-chat job `4b98776f-d422-4bfb-8a6a-7aef73c31bf6` reached `https://chatgpt.com/c/6a2ac99d-fc5c-83e8-88d7-5e1e8f427499` and completed; same-thread follow-up job `abb4f590-96a1-4aab-b91a-c0a7cc15a162` completed on the unchanged conversation URL after send-acceptance evidence
634
638
  - Pi 0.79.0 release gate: `npm run release:check` passed on 2026-06-08, including `verify:oracle` plus Crabbox macOS, Ubuntu, and Windows native `platform-build` and `real-extension` suites
635
639
  - Pi 0.79.0 platform artifacts: `.artifacts/platform-smoke/run-1780938522145-50q2f2` (macOS platform-build), `.artifacts/platform-smoke/run-1780938572090-bi87g5` (macOS real-extension), `.artifacts/platform-smoke/run-1780938542847-quridb` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1780938587248-c8uo4c` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1780938585007-l0xapp` (Windows native platform-build), `.artifacts/platform-smoke/run-1780938820527-c1j8tt` (Windows native real-extension)
636
640
  - Pi 0.79.0 isolated local-extension model-agent smoke: `.artifacts/real-smoke/run-1780935835596-pfbn5o` passed with `PI_ORACLE_REAL_TEST_MODEL_AGENT=1 npm run smoke:real:source`
@@ -27,18 +27,13 @@ The extension is loaded from the local checkout with:
27
27
  pi --approve --no-extensions -e "$REPO/extensions/oracle/index.ts"
28
28
  ```
29
29
 
30
- That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79.0+: the test fixture is this trusted checkout, and non-interactive/scripted validation must not block on the project-trust prompt.
30
+ That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79.1+: the test fixture is this trusted checkout, and non-interactive/scripted validation must not block on the project-trust prompt.
31
31
 
32
- If you also need the in-repo `/oracle` prompt template, load it explicitly instead of installing this repository as a project-local package:
32
+ The local extension now intercepts TUI `/oracle` and `/oracle-followup` before prompt-template expansion, re-injects the compact slash request as the visible user message for prompt-history/up-arrow recall, and reads the in-repo prompt files as hidden dispatch instructions, so do not pass `--prompt-template` for normal local-extension validation. In print/json/rpc modes, the extension contributes the prompt templates itself.
33
33
 
34
- ```bash
35
- pi --approve --no-extensions -e "$REPO/extensions/oracle/index.ts" \
36
- --no-prompt-templates --prompt-template "$REPO/prompts/oracle.md"
37
- ```
34
+ Do not add `https://github.com/fitchmultz/pi-oracle` to this repository's `.pi/settings.json` just to test local oracle changes. If you already keep `npm:pi-oracle` installed globally, mixing the global npm package with a project-local git package creates two distinct package identities and can trigger prompt/tool conflicts. Use the explicit CLI extension flag above instead.
38
35
 
39
- Do not add `https://github.com/fitchmultz/pi-oracle` to this repository's `.pi/settings.json` just to test local oracle changes. If you already keep `npm:pi-oracle` installed globally, mixing the global npm package with a project-local git package creates two distinct package identities and can trigger prompt/tool conflicts. Use the explicit CLI resource flags above instead.
40
-
41
- `oracle_submit` now preflights a missing or unreadable auth seed profile before it creates an archive or persists a job. For archive-inspection smoke tests that intentionally run without real auth, create an empty isolated seed-profile directory under the temporary agent dir so submission can proceed far enough to write the archive while still staying isolated from your normal Chrome state.
36
+ `oracle_submit` now preflights missing, unreadable, or unverified auth seed profiles before it creates an archive or persists a job. For archive-inspection smoke tests that intentionally run without real auth, use `oracle_preflight` for the blocker path or create a test seed only in a purpose-built fixture that includes the `.oracle-seed-generation` marker.
42
37
 
43
38
  ## Preset requirement
44
39
 
@@ -81,7 +76,12 @@ mkdir -p \
81
76
  "$TEST2_AGENT" "$TEST2_SESSIONS" "$TEST2_JOBS" \
82
77
  "$FIXTURE" "$OUTSIDE"
83
78
 
84
- mkdir -p "$TEST1_AGENT/extensions/oracle-auth-seed-profile"
79
+ mkdir -p \
80
+ "$TEST1_AGENT/extensions/oracle-auth-seed-profile" \
81
+ "$TEST2_AGENT/extensions/oracle-auth-seed-profile"
82
+ touch \
83
+ "$TEST1_AGENT/extensions/oracle-auth-seed-profile/.oracle-seed-generation" \
84
+ "$TEST2_AGENT/extensions/oracle-auth-seed-profile/.oracle-seed-generation"
85
85
 
86
86
  echo 'secret' > "$OUTSIDE/secret.txt"
87
87
  ln -s "$OUTSIDE" "$FIXTURE/linked-outside"
@@ -162,29 +162,30 @@ Expected behavior:
162
162
  Notes:
163
163
 
164
164
  - this smoke test does not require `/oracle-auth`
165
- - the snippet creates an empty isolated auth seed profile for `TEST1_AGENT` because `oracle_submit` now rejects a missing seed profile before archiving
166
- - with that empty seed profile, the worker still fails later due to missing real auth, which is useful because the archive remains on disk for inspection
165
+ - the snippet creates an isolated test auth seed profile plus `.oracle-seed-generation` marker for `TEST1_AGENT` because `oracle_submit` now rejects missing or unverified seed profiles before archiving
166
+ - with that marker-only seed profile, the worker still fails later due to missing real auth, which is useful because the archive remains on disk for inspection
167
167
 
168
168
  ### Test 2: symlink escape rejection
169
169
 
170
170
  Expected behavior:
171
171
 
172
172
  - `oracle_submit` rejects `linked-outside/secret.txt`
173
+ - the snippet creates the same marker-only isolated auth seed profile for `TEST2_AGENT` so the test reaches archive input validation
173
174
  - the error should say the archive input must resolve inside the project cwd without symlink escapes
174
175
  - no oracle job directory should be created for the rejected submit
175
176
 
176
- ## Testing local `/oracle` prompt changes too
177
+ ## Testing local `/oracle` command-prompt changes too
177
178
 
178
- The main smoke test above calls `oracle_submit` directly, so it only needs the local extension entrypoint. If you also changed `prompts/oracle.md`, start the isolated session with the local prompt template explicitly loaded:
179
+ The main smoke test above calls `oracle_submit` directly, so it only needs the local extension entrypoint. If you also changed `prompts/oracle.md`, start the isolated session with the same local extension entrypoint; the extension reads the in-repo prompt file as hidden command-dispatch instructions:
179
180
 
180
181
  ```bash
181
- LOCAL_ORACLE_PI_CMD="pi --approve --session-dir '$TEST1_SESSIONS' --no-extensions -e '$REPO/extensions/oracle/index.ts' --no-prompt-templates --prompt-template '$REPO/prompts/oracle.md'"
182
+ LOCAL_ORACLE_PI_CMD="pi --approve --session-dir '$TEST1_SESSIONS' --no-extensions -e '$REPO/extensions/oracle/index.ts'"
182
183
  TMUX_CMD1="cd '$REPO' && env PI_CODING_AGENT_DIR='$TEST1_AGENT' PI_ORACLE_JOBS_DIR='$TEST1_JOBS' PATH='$PATH' $LOCAL_ORACLE_PI_CMD"
183
184
  ```
184
185
 
185
- Use the same pattern for additional sessions, swapping the session/job directories as needed. This keeps the test on the in-repo extension and in-repo prompt template without depending on `.pi/settings.json` package entries.
186
+ Use the same pattern for additional sessions, swapping the session/job directories as needed. This keeps the test on the in-repo extension and hidden in-repo command prompt without depending on `.pi/settings.json` package entries.
186
187
 
187
- `/oracle` now starts by calling `oracle_preflight`. If you want the prompt flow to proceed past that early guard in an isolated test without using your normal auth state, create an empty isolated auth seed profile first (for example `mkdir -p "$TEST1_AGENT/extensions/oracle-auth-seed-profile"`) or run `/oracle-auth` in the isolated agent dir.
188
+ `/oracle` now starts by calling `oracle_preflight`. If you want the command flow to proceed past that early guard in an isolated test without using your normal auth state, run `/oracle-auth` in the isolated agent dir or create a purpose-built verified seed fixture with `.oracle-seed-generation`.
188
189
 
189
190
  ## Additional failure-mode smoke tests
190
191
 
@@ -90,7 +90,7 @@ On each required target, `platform-build`:
90
90
  5. runs `npm pack`;
91
91
  6. creates a fresh target-local pi project;
92
92
  7. runs `npm install --no-save <packed tarball>`;
93
- 8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79.0 project-trust gating intentionally trusts the temporary fixture;
93
+ 8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79.1 project-trust gating intentionally trusts the temporary fixture;
94
94
  9. runs `pi list --approve`;
95
95
  10. asserts the installed package came from `node_modules/pi-oracle` and did not use `pi -e` / source-extension shortcuts.
96
96
 
@@ -3,6 +3,7 @@
3
3
  // Scope: Extension entrypoint only; lifecycle mutation lives in lib modules and browser execution lives in worker scripts.
4
4
  // Usage: Loaded by pi as the extension module declared in package.json.
5
5
  // Invariants/Assumptions: Oracle only runs against persisted sessions, and startup maintenance should be best-effort without breaking session initialization.
6
+ import { readFileSync } from "node:fs";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { dirname, join } from "node:path";
8
9
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -10,15 +11,59 @@ import { loadOracleConfig } from "./lib/config.js";
10
11
  import { registerOracleCommands } from "./lib/commands.js";
11
12
  import { getSessionFile, pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./lib/jobs.js";
12
13
  import { isLockTimeoutError, withGlobalReconcileLock } from "./lib/locks.js";
13
- import { refreshOracleStatus, startPoller, stopPoller } from "./lib/poller.js";
14
+ import { refreshOracleStatus, setOracleReadiness, startPoller, stopPoller } from "./lib/poller.js";
14
15
  import { promoteQueuedJobs } from "./lib/queue.js";
15
- import { hasPersistedSessionFile } from "./lib/runtime.js";
16
+ import { assertOracleSubmitPrerequisites, hasPersistedSessionFile } from "./lib/runtime.js";
16
17
  import { registerOracleTools } from "./lib/tools.js";
17
18
 
19
+ function readPromptTemplate(path: string): string | undefined {
20
+ try {
21
+ return readFileSync(path, "utf8").replace(/^---\n[\s\S]*?\n---\n/, "");
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function oracleReadinessFromError(error: unknown): "auth_needed" | "config_error" {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return /auth seed profile/i.test(message) ? "auth_needed" : "config_error";
30
+ }
31
+
32
+ function isProjectTrusted(ctx: ExtensionContext): boolean {
33
+ return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
34
+ }
35
+
36
+ function expandOraclePromptTemplate(source: string, args: string): string {
37
+ return source.replaceAll("$@", args).replaceAll("$ARGUMENTS", args);
38
+ }
39
+
40
+ function parseOracleInput(text: string): { command: "oracle" | "oracle-followup"; args: string } | undefined {
41
+ const match = text.match(/^\/(oracle(?:-followup)?)(?:\s+([\s\S]*))?$/);
42
+ if (!match) return undefined;
43
+ return { command: match[1] as "oracle" | "oracle-followup", args: (match[2] ?? "").trim() };
44
+ }
45
+
46
+ function formatOracleUserCommand(command: "oracle" | "oracle-followup", args: string): string {
47
+ return `/${command} ${args}`;
48
+ }
49
+
50
+ function oracleDispatchMessage(command: "oracle" | "oracle-followup", args: string, template: string) {
51
+ return {
52
+ customType: "oracle-dispatch-request",
53
+ content: expandOraclePromptTemplate(template, args),
54
+ display: false,
55
+ details: { command, userRequest: args },
56
+ };
57
+ }
58
+
18
59
  export default function oracleExtension(pi: ExtensionAPI) {
19
60
  const extensionDir = dirname(fileURLToPath(import.meta.url));
20
61
  const workerPath = join(extensionDir, "worker", "run-job.mjs");
21
62
  const authWorkerPath = join(extensionDir, "worker", "auth-bootstrap.mjs");
63
+ const promptDir = join(extensionDir, "..", "..", "prompts");
64
+
65
+ const oraclePrompt = readPromptTemplate(join(promptDir, "oracle.md"));
66
+ const oracleFollowupPrompt = readPromptTemplate(join(promptDir, "oracle-followup.md"));
22
67
 
23
68
  registerOracleCommands(pi, authWorkerPath, workerPath);
24
69
  registerOracleTools(pi, workerPath, authWorkerPath);
@@ -49,7 +94,11 @@ export default function oracleExtension(pi: ExtensionAPI) {
49
94
  return;
50
95
  }
51
96
 
52
- const config = loadOracleConfig(ctx.cwd);
97
+ const config = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isProjectTrusted(ctx) });
98
+ setOracleReadiness(ctx, "loaded");
99
+ void assertOracleSubmitPrerequisites(config)
100
+ .then(() => setOracleReadiness(ctx, "ready"))
101
+ .catch((error) => setOracleReadiness(ctx, oracleReadinessFromError(error)));
53
102
  void runStartupMaintenance(ctx).catch((error) => {
54
103
  const message = `Oracle startup maintenance failed: ${error instanceof Error ? error.message : String(error)}`;
55
104
  console.error(message);
@@ -60,13 +109,44 @@ export default function oracleExtension(pi: ExtensionAPI) {
60
109
  } catch (error) {
61
110
  const message = error instanceof Error ? error.message : String(error);
62
111
  stopPoller(ctx);
112
+ setOracleReadiness(ctx, "config_error");
63
113
  if (ctx.hasUI) {
64
- ctx.ui.setStatus("oracle", ctx.ui.theme.fg("error", "oracle: config error"));
65
114
  ctx.ui.notify(message, "warning");
66
115
  }
67
116
  }
68
117
  }
69
118
 
119
+ pi.on("resources_discover", async (_event, ctx) => {
120
+ return ["print", "json", "rpc"].includes(ctx.mode) ? { promptPaths: [promptDir] } : undefined;
121
+ });
122
+
123
+ pi.on("before_agent_start", async (event) => {
124
+ const parsed = parseOracleInput(event.prompt);
125
+ if (!parsed?.args || (parsed.command === "oracle-followup" && !/^\S+\s+\S/.test(parsed.args))) return;
126
+ const template = parsed.command === "oracle" ? oraclePrompt : oracleFollowupPrompt;
127
+ if (!template?.trim()) return;
128
+ return { message: oracleDispatchMessage(parsed.command, parsed.args, template) };
129
+ });
130
+
131
+ pi.on("input", (event, ctx) => {
132
+ if (ctx.mode !== "tui" || event.source !== "interactive") return { action: "continue" };
133
+ const parsed = parseOracleInput(event.text);
134
+ if (!parsed) return { action: "continue" };
135
+ if (!parsed.args || (parsed.command === "oracle-followup" && !/^\S+\s+\S/.test(parsed.args))) {
136
+ ctx.ui.notify(parsed.command === "oracle" ? "Usage: /oracle <request>" : "Usage: /oracle-followup <job-id> <request>", "warning");
137
+ return { action: "handled" };
138
+ }
139
+ const template = parsed.command === "oracle" ? oraclePrompt : oracleFollowupPrompt;
140
+ if (!template?.trim()) {
141
+ ctx.ui.notify(`/${parsed.command} is unavailable because its internal dispatch prompt could not be loaded.`, "warning");
142
+ return { action: "handled" };
143
+ }
144
+ ctx.ui.notify("Preparing oracle job… running preflight", "info");
145
+ const delivery = event.streamingBehavior ? { deliverAs: event.streamingBehavior } : undefined;
146
+ pi.sendUserMessage(formatOracleUserCommand(parsed.command, parsed.args), delivery);
147
+ return { action: "handled" };
148
+ });
149
+
70
150
  pi.on("session_start", async (_event, ctx) => {
71
151
  startPollerForContext(ctx);
72
152
  });
@@ -4,14 +4,14 @@
4
4
  // Usage: Imported by oracle commands and tools whenever the shared oracle auth seed profile must be refreshed.
5
5
  // Invariants/Assumptions: Auth bootstrap runs under the global reconcile lock when available, uses the effective oracle config for the current workspace root, and returns the worker's stdout/stderr message verbatim on success or failure.
6
6
  import { spawn } from "node:child_process";
7
- import { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig, resolveOracleConfigForProvider, type OracleProvider } from "./config.js";
7
+ import { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig, resolveOracleConfigForProvider, type OracleConfigLoadOptions, type OracleProvider } from "./config.js";
8
8
  import { pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./jobs.js";
9
9
  import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
10
10
 
11
- export async function runOracleAuthBootstrap(authWorkerPath: string, cwd: string, provider?: OracleProvider): Promise<string> {
12
- const baseConfig = loadOracleConfig(cwd);
11
+ export async function runOracleAuthBootstrap(authWorkerPath: string, cwd: string, provider?: OracleProvider, configOptions?: OracleConfigLoadOptions): Promise<string> {
12
+ const baseConfig = loadOracleConfig(cwd, configOptions);
13
13
  const config = resolveOracleConfigForProvider(baseConfig, provider ?? baseConfig.defaults.provider);
14
- const configLoad = getOracleConfigLoadDetails(cwd);
14
+ const configLoad = getOracleConfigLoadDetails(cwd, configOptions);
15
15
  const authConfigGuidance = {
16
16
  ...configLoad,
17
17
  remediation: formatOracleAuthConfigRemediation(configLoad),
@@ -27,6 +27,11 @@ import { refreshOracleStatus } from "./poller.js";
27
27
  import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
28
28
  import { getProjectId } from "./runtime.js";
29
29
 
30
+ export interface OracleCommandPromptTemplates {
31
+ oracle?: string;
32
+ oracleFollowup?: string;
33
+ }
34
+
30
35
  async function summarizeJob(jobId: string, options?: { responsePreview?: boolean }): Promise<string> {
31
36
  const job = readJob(jobId);
32
37
  if (!job) return `Oracle job ${jobId} not found.`;
@@ -74,18 +79,30 @@ function parseOracleAuthProvider(args: string): OracleProvider | undefined {
74
79
  throw new Error("Usage: /oracle-auth [chatgpt|grok]");
75
80
  }
76
81
 
77
- export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string): void {
82
+ function isProjectTrusted(ctx: ExtensionCommandContext): boolean {
83
+ return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
84
+ }
85
+
86
+ function emitCommandOutput(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
87
+ if (ctx.mode === "print") {
88
+ process.stdout.write(`${message}\n`);
89
+ return;
90
+ }
91
+ ctx.ui.notify(message, level);
92
+ }
93
+
94
+ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string, _promptTemplates: OracleCommandPromptTemplates = {}): void {
78
95
  pi.registerCommand("oracle-auth", {
79
96
  description: "Sync ChatGPT or Grok cookies from the configured local browser profile into the provider auth seed profile",
80
97
  handler: async (args, ctx) => {
81
98
  try {
82
99
  const provider = parseOracleAuthProvider(args);
83
100
  const providerLabel = provider === "grok" ? "Grok" : provider === "chatgpt" ? "ChatGPT" : "configured provider";
84
- ctx.ui.notify(`Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
85
- const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd, provider);
86
- ctx.ui.notify(result, "info");
101
+ emitCommandOutput(ctx, `Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
102
+ const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd, provider, { projectConfigTrusted: isProjectTrusted(ctx) });
103
+ emitCommandOutput(ctx, result, "info");
87
104
  } catch (error) {
88
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
105
+ emitCommandOutput(ctx, error instanceof Error ? error.message : String(error), "warning");
89
106
  }
90
107
  },
91
108
  });
@@ -96,12 +113,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
96
113
  const explicitJobId = args.trim();
97
114
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
98
115
  if (!jobId) {
99
- ctx.ui.notify("No oracle jobs found for this project", "info");
116
+ emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
100
117
  return;
101
118
  }
102
119
  const job = readScopedJob(jobId, ctx.cwd);
103
120
  if (!job) {
104
- ctx.ui.notify(`Oracle job ${jobId} was not found in this project`, "warning");
121
+ emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
105
122
  return;
106
123
  }
107
124
  if (isTerminalOracleJob(job)) {
@@ -113,7 +130,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
113
130
  }
114
131
  const summary = await summarizeJob(job.id);
115
132
  const recentJobs = !explicitJobId ? listRecentJobIds(ctx.cwd) : undefined;
116
- ctx.ui.notify([summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
133
+ emitCommandOutput(ctx, [summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
117
134
  },
118
135
  });
119
136
 
@@ -123,12 +140,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
123
140
  const explicitJobId = args.trim();
124
141
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
125
142
  if (!jobId) {
126
- ctx.ui.notify("No oracle jobs found for this project", "info");
143
+ emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
127
144
  return;
128
145
  }
129
146
  const job = readScopedJob(jobId, ctx.cwd);
130
147
  if (!job) {
131
- ctx.ui.notify(`Oracle job ${jobId} was not found in this project`, "warning");
148
+ emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
132
149
  return;
133
150
  }
134
151
  if (isTerminalOracleJob(job)) {
@@ -138,7 +155,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
138
155
  cwd: ctx.cwd,
139
156
  });
140
157
  }
141
- ctx.ui.notify(await summarizeJob(job.id, { responsePreview: true }), "info");
158
+ emitCommandOutput(ctx, await summarizeJob(job.id, { responsePreview: true }), "info");
142
159
  },
143
160
  });
144
161
 
@@ -147,17 +164,21 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
147
164
  handler: async (args, ctx) => {
148
165
  const jobId = args.trim();
149
166
  if (!jobId) {
150
- ctx.ui.notify("Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
167
+ emitCommandOutput(ctx, "Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
151
168
  return;
152
169
  }
153
170
 
154
171
  const job = readScopedJob(jobId, ctx.cwd);
155
172
  if (!job) {
156
- ctx.ui.notify(`Oracle job ${jobId} not found in this project`, "warning");
173
+ emitCommandOutput(ctx, `Oracle job ${jobId} not found in this project`, "warning");
157
174
  return;
158
175
  }
159
176
  if (!isOpenOracleJob(job)) {
160
- ctx.ui.notify(`Oracle job ${jobId} is not cancellable (${job.status})`, "info");
177
+ 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");
179
+ } else {
180
+ emitCommandOutput(ctx, `Oracle job ${jobId} is not cancellable (${job.status})`, "info");
181
+ }
161
182
  return;
162
183
  }
163
184
 
@@ -166,7 +187,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
166
187
  await promoteQueuedJobs({ workerPath, source: "oracle_cancel_command" });
167
188
  }
168
189
  refreshOracleStatus(ctx);
169
- ctx.ui.notify(formatOracleCancelOutcome(cancelled), "info");
190
+ emitCommandOutput(ctx, formatOracleCancelOutcome(cancelled), "info");
170
191
  },
171
192
  });
172
193
 
@@ -175,19 +196,20 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
175
196
  handler: async (args, ctx: ExtensionCommandContext) => {
176
197
  const target = args.trim();
177
198
  if (!target) {
178
- ctx.ui.notify("Usage: /oracle-clean <job-id|all>", "warning");
199
+ emitCommandOutput(ctx, "Usage: /oracle-clean <job-id|all>", "warning");
179
200
  return;
180
201
  }
181
202
 
182
203
  const jobs = target === "all" ? listJobsForCwd(ctx.cwd) : [readScopedJob(target, ctx.cwd)].filter(Boolean);
183
204
  if (jobs.length === 0) {
184
- ctx.ui.notify("No matching oracle jobs found", "warning");
205
+ emitCommandOutput(ctx, "No matching oracle jobs found", "warning");
185
206
  return;
186
207
  }
187
208
 
188
209
  const nonTerminalJobs = jobs.filter((job): job is NonNullable<typeof job> => Boolean(job && !isTerminalOracleJob(job)));
189
210
  if (nonTerminalJobs.length > 0) {
190
- ctx.ui.notify(
211
+ emitCommandOutput(
212
+ ctx,
191
213
  `Refusing to remove non-terminal oracle job${nonTerminalJobs.length === 1 ? "" : "s"}: ${nonTerminalJobs.map((job) => job.id).join(", ")}`,
192
214
  "warning",
193
215
  );
@@ -217,10 +239,14 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
217
239
 
218
240
  refreshOracleStatus(ctx);
219
241
  const warningSuffix = cleanupWarnings.length > 0 ? ` Cleanup blockers/warnings:\n${cleanupWarnings.join("\n")}` : "";
220
- const removalSummary = removedCount === jobs.length
221
- ? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
222
- : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} due to cleanup blockers or warnings.`;
223
- ctx.ui.notify(`${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
242
+ const retentionWarning = cleanupWarnings.find((warning) => warning.includes("post-send retention grace window") && warning.includes("Retry after "));
243
+ const retryAfter = retentionWarning?.match(/Retry after ([^ ]+)/)?.[1];
244
+ const removalSummary = retryAfter && removedCount < jobs.length
245
+ ? `Job retained for wake-up safety. Retry cleanup after ${retryAfter}.`
246
+ : removedCount === jobs.length
247
+ ? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
248
+ : `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");
224
250
  },
225
251
  });
226
252
  }