pi-oracle 0.4.0 → 0.6.0

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
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.6.0 - 2026-04-13
6
+
7
+ ### Added
8
+ - `oracle_auth`, an agent-facing tool that mirrors `/oracle-auth` so oracle runs can refresh the shared ChatGPT auth seed profile before a single retry when stale auth blocks execution
9
+
10
+ ### Changed
11
+ - `/oracle` and `/oracle-followup` now follow a stricter preflight-first flow, bias toward context-rich archive selection up to the 250 MB ceiling, and explicitly allow one `oracle_auth` refresh before retrying stale-auth/login-required failures
12
+ - package metadata now follows the current pi package dependency guidance by publishing `@mariozechner/pi-coding-agent` and `@sinclair/typebox` as peer dependencies while keeping local typechecking/dev resolution intact
13
+
14
+ ### Fixed
15
+ - `/oracle` no-session and missing-seed flows now stop before unnecessary repo exploration, and prompt-template guidance keeps relevant surrounding archive context instead of over-optimizing for minimal slices when extra context still fits
16
+ - oracle model selection now recognizes ChatGPT family controls exposed as radios/menu items plus durable closed-chip states like `Extended thinking` and `Extended Pro`, so remembered new-chat defaults no longer derail preset configuration or ready-state detection
17
+ - authenticated ready-state detection now accepts extended closed-chip model controls during auth/bootstrap verification, preventing false login/setup failures when ChatGPT remembers a non-default preset
18
+
19
+ ## 0.5.0 - 2026-04-12
20
+
21
+ ### Added
22
+ - `oracle_preflight`, a lightweight readiness tool that lets `/oracle` fail fast on missing persisted-session or local auth/config blockers before archive/context work begins
23
+
24
+ ### Changed
25
+ - `/oracle` now follows a stricter preflight-first flow, biases toward minimal context gathering for explicitly narrow requests, and prefers the configured default model unless a different preset is clearly needed
26
+ - `oracle_read`, `/oracle-status`, and wake-up messaging now keep the true terminal event prominent, separate wake-up bookkeeping from failure state, and stop implying that a missing `response.md` is ready
27
+ - `/oracle-auth` failure guidance now reports the effective agent config path for the active agent dir and explains when a project config was also read but could not override `auth.*`
28
+ - `/oracle-clean` now documents the short post-send retention grace window and returns a retry-after timestamp when a terminal job is still intentionally retained
29
+
30
+ ### Fixed
31
+ - `oracle_submit` now rejects locally knowable auth-seed blockers before archive creation or job persistence while still preserving direct archive-input validation errors like symlink escapes
32
+ - oracle tool results now use consistent structured `details.job` / `details.error` payloads and preserve `isError` for structured failures through the tool-result hook
33
+ - `/oracle` no-session and missing-seed flows now stop before unnecessary repo exploration, and narrow prompt-template runs dispatch more quickly with smaller archives when the user scope is explicit
34
+ - repeated oracle sanity runs now quiesce background pollers before isolated-state teardown so release verification no longer emits a noisy temp-lock ENOENT race
35
+
3
36
  ## 0.4.0 - 2026-04-12
4
37
 
5
38
  ### Added
package/README.md CHANGED
@@ -42,12 +42,17 @@ pi install https://github.com/fitchmultz/pi-oracle
42
42
 
43
43
  ## Quickstart
44
44
 
45
- 1. Make sure ChatGPT already works in your local Chrome profile.
46
- 2. Make sure these are installed: Google Chrome, `agent-browser`, `tar`, and `zstd`.
47
- 3. Optional: create `~/.pi/agent/extensions/oracle.json` if you want non-default settings.
48
- 4. Run `/oracle-auth`.
49
- 5. Run `/oracle Review the current pending changes. Include the whole repo unless a narrower archive is clearly better.`
50
- 6. Wait for a best-effort wake-up, or check `/oracle-status`.
45
+ 1. Start a normal persisted `pi` session. Do not use `pi --no-session` for oracle.
46
+ 2. Make sure ChatGPT already works in your local Chrome profile.
47
+ 3. Make sure these are installed: Google Chrome, `agent-browser`, `tar`, and `zstd`.
48
+ 4. Optional: create `~/.pi/agent/extensions/oracle.json` if you want non-default settings.
49
+ 5. Run `/oracle-auth`.
50
+ 6. Run `/oracle Review the current pending changes. Include the whole repo unless a narrower archive is clearly better.`
51
+ 7. Wait for a best-effort wake-up, or check `/oracle-status`.
52
+
53
+ The `/oracle` prompt now runs an early oracle preflight before it gathers repo context, so missing persisted-session or local auth/config blockers fail before the agent spends time reading files.
54
+
55
+ For explicitly narrow requests, `/oracle` should still prefer a context-rich relevant archive up to the 250 MB ceiling, including nearby tests, docs, config, and adjacent modules when that can improve answer quality. Reserve tightly minimal archives for an explicit user request for a tight archive, privacy-sensitive material, or size-constrained cases. It should also omit `preset` and use the configured default model unless the task clearly needs a different one.
51
56
 
52
57
  If you miss the wake-up, the result is still saved durably in the oracle job directory and can be read later.
53
58
 
@@ -61,11 +66,21 @@ If you miss the wake-up, the result is still saved durably in the oracle job dir
61
66
  /oracle Read the codebase and explain the highest-risk auth/session failure modes, including what to test before shipping.
62
67
  ```
63
68
 
69
+ ```text
70
+ /oracle Explain the README guidance for /oracle-clean retention grace. Archive README.md plus any nearby docs or implementation files that help answer accurately.
71
+ ```
72
+
73
+ ```text
74
+ /oracle-followup <job-id> Tighten the migration plan around rollback risk, and include the most relevant surrounding files/docs as long as the archive stays comfortably within the limit.
75
+ ```
76
+
77
+ After a job finishes, use `/oracle-followup <job-id> <request>` to continue the same ChatGPT thread without hand-writing the low-level `followUpJobId` tool parameter.
78
+
64
79
  ## High-level flow
65
80
 
66
81
  ```mermaid
67
82
  flowchart LR
68
- A["/oracle request"] --> B["Agent gathers repo context"]
83
+ A["/oracle request"] --> B["Agent preflights, then gathers a context-rich relevant repo slice"]
69
84
  B --> C["oracle_submit builds archive"]
70
85
  C --> D["Detached worker starts isolated ChatGPT runtime"]
71
86
  D --> E["Archive + prompt sent to ChatGPT.com"]
@@ -79,12 +94,16 @@ If concurrency is full, the job is queued and starts automatically later.
79
94
 
80
95
  User-facing commands:
81
96
  - `/oracle <request>` — prompt template that tells the agent to gather context and dispatch an oracle job
97
+ - `/oracle-followup <job-id> <request>` — prompt template that continues an earlier oracle job in the same ChatGPT thread
82
98
  - `/oracle-auth` — sync ChatGPT cookies from your real Chrome profile into the isolated oracle auth profile
83
- - `/oracle-status [job-id]` — inspect job status
84
- - `/oracle-cancel [job-id]` — cancel queued or active job
85
- - `/oracle-clean <job-id|all>` — remove temp files for terminal jobs
99
+ - `/oracle-read [job-id]` — inspect job status plus the saved response preview
100
+ - `/oracle-status [job-id]` — inspect job status and list recent job ids when no explicit id is given
101
+ - `/oracle-cancel <job-id>` — cancel a queued or active job by id
102
+ - `/oracle-clean <job-id|all>` — remove temp files for terminal jobs; recently woken terminal jobs may stay retained briefly and return a retry-after hint
86
103
 
87
104
  Agent-facing tools:
105
+ - `oracle_preflight`
106
+ - `oracle_auth`
88
107
  - `oracle_submit`
89
108
  - `oracle_read`
90
109
  - `oracle_cancel`
@@ -142,7 +161,11 @@ Project config should only override safe, non-privileged settings.
142
161
  - Jobs persist their response and any artifacts under `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` by default.
143
162
  - Jobs can queue automatically if runtime capacity is full.
144
163
  - Completion delivery into `pi` is best-effort wake-up based.
145
- - If you miss the wake-up, use `oracle_read(jobId)` or `/oracle-status`.
164
+ - If you miss the wake-up, use `/oracle-read [job-id]` to inspect the saved response preview.
165
+ - `/oracle-status [job-id]` still shows saved job metadata and lists recent job ids when you omit the id.
166
+ - Agent callers can use `oracle_read({ jobId })`.
167
+ - If a prior oracle run failed because ChatGPT login was required or the worker explicitly said to rerun `/oracle-auth`, agent callers can run `oracle_auth({})` once and then retry the submission once.
168
+ - `/oracle-clean` can still refuse a terminal job briefly after a wake-up send so saved response/artifact paths survive the follow-up turn; when that guard applies, it returns the next eligible cleanup time.
146
169
 
147
170
  ## Requirements
148
171
 
@@ -174,9 +197,17 @@ Project config should only override safe, non-privileged settings.
174
197
 
175
198
  ### A job finished but no wake-up arrived
176
199
 
177
- - Use `/oracle-status [job-id]` or `oracle_read(jobId)`.
200
+ - Use `/oracle-read [job-id]` to inspect the saved response preview.
201
+ - Use `/oracle-status [job-id]` when you want status metadata or need help finding a job id.
202
+ - Agent callers can use `oracle_read({ jobId })` if they need tool output in the current turn.
178
203
  - Results are still saved on disk even if the reminder turn does not land.
179
204
 
205
+ ### `/oracle-clean` refuses a terminal job right after completion
206
+
207
+ - This can happen during the short post-send retention grace window after a wake-up was sent.
208
+ - The command now returns a `Retry after ...` timestamp when that guard is active.
209
+ - Wait until that time, then rerun `/oracle-clean [job-id|all]`.
210
+
180
211
  ### `agent-browser`, `tar`, or `zstd` is missing
181
212
 
182
213
  - Install the missing local dependency and rerun the command.
@@ -65,20 +65,31 @@ The extension now follows the current `pi` session lifecycle model:
65
65
  - implemented as a prompt template, not an extension command
66
66
  - asks the agent to gather context and dispatch an oracle job
67
67
  - intentionally uses native pi prompt/template queueing so submissions survive streaming and compaction
68
+ - `/oracle-followup <job-id> <request>`
69
+ - implemented as a prompt template, not an extension command
70
+ - asks the agent to continue an earlier oracle job in the same ChatGPT thread via `followUpJobId`
71
+ - keeps same-thread continuation available to normal users without requiring raw tool-call syntax
68
72
 
69
73
  ### Commands
70
74
 
71
75
  - `/oracle-auth`
72
76
  - syncs ChatGPT cookies from the user’s real Chrome into the isolated oracle profile and verifies them there
77
+ - `/oracle-read [job-id]`
78
+ - shows job status plus the saved response preview
73
79
  - `/oracle-status [job-id]`
74
- - shows job status
75
- - `/oracle-cancel [job-id]`
76
- - cancels a queued or active job
80
+ - shows job status and lists recent job ids when the caller omits an explicit id
81
+ - `/oracle-cancel <job-id>`
82
+ - cancels a queued or active job by id; does not guess a default target
77
83
  - `/oracle-clean <job-id|all>`
78
84
  - removes temp files for terminal jobs only
79
85
 
80
86
  ### Tools
81
87
 
88
+ - `oracle_preflight`
89
+ - lightweight agent-facing readiness check for persisted-session and local oracle prerequisites
90
+ - intended to run before expensive `/oracle` context gathering
91
+ - `oracle_auth`
92
+ - agent-facing auth refresh tool that mirrors `/oracle-auth` for stale-auth recovery before a retry
82
93
  - `oracle_submit`
83
94
  - low-level agent-facing dispatch tool
84
95
  - creates archive and launches a detached worker
@@ -97,12 +108,16 @@ It expands through the prompt-template path so pi can apply its native queueing
97
108
 
98
109
  Instead it instructs the agent to:
99
110
 
100
- 1. understand the task
101
- 2. gather repo context
102
- 3. choose exact archive inputs
103
- 4. craft the oracle prompt
104
- 5. call `oracle_submit`
105
- 6. stop and wait for the completion wake-up (best-effort; durable oracle response/artifact state is already persisted outside session history)
111
+ 1. call `oracle_preflight` immediately
112
+ 2. stop right away if preflight reports the session or local oracle setup is not ready
113
+ 3. understand whether the request is explicitly narrow or genuinely broad
114
+ 4. 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
115
+ 5. gather enough repo context to submit well and bias toward context-rich archives when they fit within the 250 MB ceiling
116
+ 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
117
+ 7. if the request is broad/repo-wide, gather broader context and usually archive `.`
118
+ 8. craft the oracle prompt
119
+ 9. call `oracle_submit`
120
+ 10. stop and wait for the completion wake-up (best-effort; durable oracle response/artifact state is already persisted outside session history)
106
121
 
107
122
  ### `/oracle-auth`
108
123
 
@@ -133,7 +148,7 @@ The authenticated seed profile remains the source of truth for future oracle run
133
148
 
134
149
  ### `oracle_submit`
135
150
 
136
- Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields. Submit-time inputs accept canonical preset ids plus matching human-readable labels/common hyphen-space variants, and the tool normalizes them back to the canonical id before persisting job state.
151
+ Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields. Submit-time inputs accept canonical preset ids plus matching human-readable labels/common hyphen-space variants, and the tool normalizes them back to the canonical id before persisting job state. Prompt-template guidance biases toward omitting `preset` and using the configured default unless the task clearly needs a different model or the user explicitly asked for one. It also biases toward context-rich archives up to the 250 MB ceiling, narrowing only when the user explicitly asks for a tight archive, privacy/sensitivity requires it, or size pressure forces it.
137
152
 
138
153
  1. resolve the preset (submit-time or config default) into an execution snapshot
139
154
  2. resolve optional `followUpJobId` into a prior `chatUrl` and `conversationId`
@@ -262,7 +277,7 @@ Long-run hygiene is intentionally conservative:
262
277
 
263
278
  - runtime profiles, runtime leases, and conversation leases are cleaned immediately as part of worker/command cleanup paths
264
279
  - browser close is time-bounded so cleanup can continue even if `agent-browser close` wedges
265
- - `/oracle-clean` performs runtime cleanup before removing the persisted job directory, but refuses terminal jobs whose worker is still live or whose wake-up was just sent inside a short post-send retention grace window
280
+ - `/oracle-clean` performs runtime cleanup before removing the persisted job directory, but refuses terminal jobs whose worker is still live or whose wake-up was just sent inside a short post-send retention grace window; when blocked by that grace it returns a retry-after timestamp
266
281
  - stale lock directories are swept before reconcile maintenance
267
282
  - old auth `.staging-*` profiles are swept during `/oracle-auth` startup when the auth browser session is not still active
268
283
  - terminal job directories are retained for inspection, then pruned later based on configurable retention windows
@@ -448,6 +463,7 @@ Same-thread continuity is persisted as data, not runtime browser state.
448
463
 
449
464
  Approach:
450
465
 
466
+ - expose `/oracle-followup <job-id> <request>` as the user-facing way to continue the same ChatGPT thread later
451
467
  - store `chatUrl` only after the conversation URL stabilizes
452
468
  - derive and persist `conversationId` from that URL when possible
453
469
  - for a follow-up job, resolve `followUpJobId` to the prior `chatUrl`
@@ -467,10 +483,10 @@ The extension still uses the same general `pi`-native background completion patt
467
483
  - poller scans jobs on an interval
468
484
  - completed job durability lives in oracle job state plus saved response/artifact files, not in synthetic session-history assistant messages
469
485
  - when a matching job reaches `complete`, `failed`, or `cancelled`, the poller issues bounded best-effort wake-up reminders to whichever matching session is currently live
470
- - those wake-ups direct the receiver to `oracle_read(jobId)` as the canonical completion-consumption path, while still surfacing saved response/artifact paths as secondary context
471
- - manual `oracle_read` or `/oracle-status` inspection settles further reminder retries once the terminal job has been opened and persists provenance about which path/session settled the wake-up
486
+ - those wake-ups direct the receiver to `/oracle-read [job-id]` as the primary completion-consumption path, while still surfacing saved response/artifact paths as secondary context; `/oracle-status` remains useful for metadata and job-id discovery, and agent callers can still use `oracle_read` when they need tool output in-turn
487
+ - manual `oracle_read`, `/oracle-read`, or `/oracle-status` inspection settles further reminder retries once the terminal job has been opened and persists provenance about which path/session settled the wake-up
472
488
  - manual inspection before the first wake-up attempt is recorded separately as observation metadata and does not suppress the first reminder send
473
- - if no wake-up lands, the job remains available via `/oracle-status`, `oracle_read`, and the saved `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` response/artifact files
489
+ - if no wake-up lands, the job remains available via `/oracle-read`, `/oracle-status`, `oracle_read`, and the saved `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` response/artifact files
474
490
  - because completion delivery is best-effort, pruning uses explicit terminal-job age policy instead of pretending a durable session notification happened
475
491
  - recently sent wake-ups keep response/artifact files retained briefly so follow-up turns do not point at deleted paths if cleanup or pruning races with delivery
476
492
 
@@ -512,8 +528,8 @@ Implemented in code for the pivot and concurrency redesign:
512
528
 
513
529
  Retained from the earlier MVP:
514
530
 
515
- - `/oracle`, `/oracle-status`, `/oracle-cancel`, `/oracle-clean`
516
- - `oracle_submit`, `oracle_read`, `oracle_cancel`
531
+ - `/oracle`, `/oracle-followup`, `/oracle-read`, `/oracle-status`, `/oracle-cancel`, `/oracle-clean`
532
+ - `oracle_auth`, `oracle_submit`, `oracle_read`, `oracle_cancel`
517
533
  - detached background worker model
518
534
  - `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/...` state layout
519
535
  - shell-safe archive creation using `tar` piped to `zstd`
@@ -29,6 +29,17 @@ pi --no-extensions -e "$REPO/extensions/oracle/index.ts"
29
29
 
30
30
  That ensures the session is exercising the in-repo code, not a globally installed package.
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:
33
+
34
+ ```bash
35
+ pi --no-extensions -e "$REPO/extensions/oracle/index.ts" \
36
+ --no-prompt-templates --prompt-template "$REPO/prompts/oracle.md"
37
+ ```
38
+
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.
42
+
32
43
  ## Preset requirement
33
44
 
34
45
  Use either:
@@ -70,6 +81,8 @@ mkdir -p \
70
81
  "$TEST2_AGENT" "$TEST2_SESSIONS" "$TEST2_JOBS" \
71
82
  "$FIXTURE" "$OUTSIDE"
72
83
 
84
+ mkdir -p "$TEST1_AGENT/extensions/oracle-auth-seed-profile"
85
+
73
86
  echo 'secret' > "$OUTSIDE/secret.txt"
74
87
  ln -s "$OUTSIDE" "$FIXTURE/linked-outside"
75
88
 
@@ -149,7 +162,8 @@ Expected behavior:
149
162
  Notes:
150
163
 
151
164
  - this smoke test does not require `/oracle-auth`
152
- - without an auth seed profile, the worker fails after archive creation, which is useful because the archive remains on disk for inspection
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
153
167
 
154
168
  ### Test 2: symlink escape rejection
155
169
 
@@ -159,6 +173,19 @@ Expected behavior:
159
173
  - the error should say the archive input must resolve inside the project cwd without symlink escapes
160
174
  - no oracle job directory should be created for the rejected submit
161
175
 
176
+ ## Testing local `/oracle` prompt changes too
177
+
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
+
180
+ ```bash
181
+ LOCAL_ORACLE_PI_CMD="pi --session-dir '$TEST1_SESSIONS' --no-extensions -e '$REPO/extensions/oracle/index.ts' --no-prompt-templates --prompt-template '$REPO/prompts/oracle.md'"
182
+ 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
+
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
+
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
+
162
189
  ## Additional failure-mode smoke tests
163
190
 
164
191
  ### `/oracle-auth` should fail fast when `agent-browser` hangs
@@ -21,7 +21,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
21
21
  const authWorkerPath = join(extensionDir, "worker", "auth-bootstrap.mjs");
22
22
 
23
23
  registerOracleCommands(pi, authWorkerPath, workerPath);
24
- registerOracleTools(pi, workerPath);
24
+ registerOracleTools(pi, workerPath, authWorkerPath);
25
25
 
26
26
  async function runStartupMaintenance(ctx: ExtensionContext): Promise<void> {
27
27
  try {
@@ -0,0 +1,50 @@
1
+ // Purpose: Share oracle auth-bootstrap orchestration between slash commands and agent-facing tools.
2
+ // Responsibilities: Load effective auth guidance, run reconcile maintenance, spawn the auth bootstrap worker, and return user-facing results.
3
+ // Scope: Extension-side auth bootstrap orchestration only; browser cookie import and profile validation stay in worker/auth-bootstrap.mjs.
4
+ // Usage: Imported by oracle commands and tools whenever the shared oracle auth seed profile must be refreshed.
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
+ import { spawn } from "node:child_process";
7
+ import { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig } from "./config.js";
8
+ import { pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./jobs.js";
9
+ import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
10
+
11
+ export async function runOracleAuthBootstrap(authWorkerPath: string, cwd: string): Promise<string> {
12
+ const config = loadOracleConfig(cwd);
13
+ const configLoad = getOracleConfigLoadDetails(cwd);
14
+ const authConfigGuidance = {
15
+ ...configLoad,
16
+ remediation: formatOracleAuthConfigRemediation(configLoad),
17
+ summary: formatOracleAuthConfigSummary(configLoad),
18
+ };
19
+
20
+ try {
21
+ await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_auth", cwd }, async () => {
22
+ await reconcileStaleOracleJobs();
23
+ await pruneTerminalOracleJobs();
24
+ });
25
+ } catch (error) {
26
+ if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
27
+ }
28
+
29
+ return await new Promise<string>((resolve, reject) => {
30
+ const child = spawn(process.execPath, [authWorkerPath, JSON.stringify({ config, configLoad: authConfigGuidance })], {
31
+ cwd,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ });
34
+
35
+ let stdout = "";
36
+ let stderr = "";
37
+ child.stdout.on("data", (data) => {
38
+ stdout += String(data);
39
+ });
40
+ child.stderr.on("data", (data) => {
41
+ stderr += String(data);
42
+ });
43
+ child.on("error", (error) => reject(error));
44
+ child.on("close", (code) => {
45
+ const message = stdout.trim() || stderr.trim() || "Oracle auth bootstrap finished with no output.";
46
+ if (code === 0) resolve(message);
47
+ else reject(new Error(message));
48
+ });
49
+ });
50
+ }
@@ -3,10 +3,11 @@
3
3
  // Scope: Command-facing orchestration only; durable lifecycle mutations live in jobs/runtime/tools modules and browser execution stays in worker scripts.
4
4
  // Usage: Imported by the oracle extension entrypoint to register /oracle-* commands with pi.
5
5
  // Invariants/Assumptions: Commands operate on persisted project-scoped jobs and rely on shared observability formatting for detached-state clarity.
6
- import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
7
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
9
  import { formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
9
- import { loadOracleConfig } from "./config.js";
10
+ import { runOracleAuthBootstrap } from "./auth.js";
10
11
  import {
11
12
  cancelOracleJob,
12
13
  getJobDir,
@@ -14,7 +15,6 @@ import {
14
15
  isTerminalOracleJob,
15
16
  listJobsForCwd,
16
17
  markWakeupSettled,
17
- pruneTerminalOracleJobs,
18
18
  readJob,
19
19
  reconcileStaleOracleJobs,
20
20
  removeTerminalOracleJob,
@@ -25,13 +25,25 @@ import { refreshOracleStatus } from "./poller.js";
25
25
  import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
26
26
  import { getProjectId } from "./runtime.js";
27
27
 
28
- function summarizeJob(jobId: string): string {
28
+ async function summarizeJob(jobId: string, options?: { responsePreview?: boolean }): Promise<string> {
29
29
  const job = readJob(jobId);
30
30
  if (!job) return `Oracle job ${jobId} not found.`;
31
31
 
32
+ const responseAvailable = Boolean(job.responsePath && existsSync(job.responsePath));
33
+ let responsePreview: string | undefined;
34
+ if (options?.responsePreview && responseAvailable && job.responsePath) {
35
+ try {
36
+ responsePreview = (await readFile(job.responsePath, "utf8")).slice(0, 4000);
37
+ } catch {
38
+ responsePreview = undefined;
39
+ }
40
+ }
41
+
32
42
  return formatOracleJobSummary(job, {
33
43
  queuePosition: job.status === "queued" ? getQueuePosition(job.id) : undefined,
34
44
  artifactsPath: `${getJobDir(job.id)}/artifacts`,
45
+ responseAvailable,
46
+ responsePreview,
35
47
  });
36
48
  }
37
49
 
@@ -39,53 +51,25 @@ function getLatestJobId(cwd: string): string | undefined {
39
51
  return listJobsForCwd(cwd)[0]?.id;
40
52
  }
41
53
 
54
+ function listRecentJobIds(cwd: string, limit = 5): string | undefined {
55
+ const jobs = listJobsForCwd(cwd).slice(0, limit);
56
+ if (jobs.length === 0) return undefined;
57
+ return jobs.map((job) => `${job.id} (${job.status})`).join(", ");
58
+ }
59
+
42
60
  function readScopedJob(jobId: string, cwd: string) {
43
61
  const job = readJob(jobId);
44
62
  if (!job || job.projectId !== getProjectId(cwd)) return undefined;
45
63
  return job;
46
64
  }
47
65
 
48
- async function runAuthBootstrap(authWorkerPath: string, cwd: string): Promise<string> {
49
- const config = loadOracleConfig(cwd);
50
- try {
51
- await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_auth", cwd }, async () => {
52
- await reconcileStaleOracleJobs();
53
- await pruneTerminalOracleJobs();
54
- });
55
- } catch (error) {
56
- if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
57
- }
58
-
59
- return await new Promise<string>((resolve, reject) => {
60
- const child = spawn(process.execPath, [authWorkerPath, JSON.stringify(config)], {
61
- cwd,
62
- stdio: ["ignore", "pipe", "pipe"],
63
- });
64
-
65
- let stdout = "";
66
- let stderr = "";
67
- child.stdout.on("data", (data) => {
68
- stdout += String(data);
69
- });
70
- child.stderr.on("data", (data) => {
71
- stderr += String(data);
72
- });
73
- child.on("error", (error) => reject(error));
74
- child.on("close", (code) => {
75
- const message = stdout.trim() || stderr.trim() || "Oracle auth bootstrap finished with no output.";
76
- if (code === 0) resolve(message);
77
- else reject(new Error(message));
78
- });
79
- });
80
- }
81
-
82
66
  export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string): void {
83
67
  pi.registerCommand("oracle-auth", {
84
68
  description: "Sync ChatGPT cookies from real Chrome into the oracle auth seed profile",
85
69
  handler: async (_args, ctx) => {
86
70
  ctx.ui.notify("Syncing ChatGPT cookies from real Chrome into the oracle auth seed profile…", "info");
87
71
  try {
88
- const result = await runAuthBootstrap(authWorkerPath, ctx.cwd);
72
+ const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd);
89
73
  ctx.ui.notify(result, "info");
90
74
  } catch (error) {
91
75
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
@@ -94,7 +78,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
94
78
  });
95
79
 
96
80
  pi.registerCommand("oracle-status", {
97
- description: "Show oracle job status",
81
+ description: "Show oracle job status and recent job ids",
98
82
  handler: async (args, ctx) => {
99
83
  const explicitJobId = args.trim();
100
84
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
@@ -114,12 +98,14 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
114
98
  cwd: ctx.cwd,
115
99
  });
116
100
  }
117
- ctx.ui.notify(summarizeJob(job.id), "info");
101
+ const summary = await summarizeJob(job.id);
102
+ const recentJobs = !explicitJobId ? listRecentJobIds(ctx.cwd) : undefined;
103
+ ctx.ui.notify([summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
118
104
  },
119
105
  });
120
106
 
121
- pi.registerCommand("oracle-cancel", {
122
- description: "Cancel a queued or active oracle job",
107
+ pi.registerCommand("oracle-read", {
108
+ description: "Show oracle job status plus saved response preview",
123
109
  handler: async (args, ctx) => {
124
110
  const explicitJobId = args.trim();
125
111
  const jobId = explicitJobId || getLatestJobId(ctx.cwd);
@@ -127,8 +113,32 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
127
113
  ctx.ui.notify("No oracle jobs found for this project", "info");
128
114
  return;
129
115
  }
116
+ const job = readScopedJob(jobId, ctx.cwd);
117
+ if (!job) {
118
+ ctx.ui.notify(`Oracle job ${jobId} was not found in this project`, "warning");
119
+ return;
120
+ }
121
+ if (isTerminalOracleJob(job)) {
122
+ await markWakeupSettled(job.id, {
123
+ source: "oracle_read_command",
124
+ sessionFile: ctx.sessionManager.getSessionFile?.(),
125
+ cwd: ctx.cwd,
126
+ });
127
+ }
128
+ ctx.ui.notify(await summarizeJob(job.id, { responsePreview: true }), "info");
129
+ },
130
+ });
130
131
 
131
- const job = explicitJobId ? readScopedJob(jobId, ctx.cwd) : readJob(jobId);
132
+ pi.registerCommand("oracle-cancel", {
133
+ description: "Cancel a queued or active oracle job by id",
134
+ handler: async (args, ctx) => {
135
+ const jobId = args.trim();
136
+ if (!jobId) {
137
+ ctx.ui.notify("Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
138
+ return;
139
+ }
140
+
141
+ const job = readScopedJob(jobId, ctx.cwd);
132
142
  if (!job) {
133
143
  ctx.ui.notify(`Oracle job ${jobId} not found in this project`, "warning");
134
144
  return;
@@ -151,7 +161,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
151
161
  });
152
162
 
153
163
  pi.registerCommand("oracle-clean", {
154
- description: "Remove oracle temp files for a job or all project jobs",
164
+ description: "Remove oracle temp files for terminal jobs; recently woken jobs may stay retained briefly",
155
165
  handler: async (args, ctx: ExtensionCommandContext) => {
156
166
  const target = args.trim();
157
167
  if (!target) {
@@ -196,10 +206,10 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
196
206
  }
197
207
 
198
208
  refreshOracleStatus(ctx);
199
- const warningSuffix = cleanupWarnings.length > 0 ? ` Cleanup warnings:\n${cleanupWarnings.join("\n")}` : "";
209
+ const warningSuffix = cleanupWarnings.length > 0 ? ` Cleanup blockers/warnings:\n${cleanupWarnings.join("\n")}` : "";
200
210
  const removalSummary = removedCount === jobs.length
201
211
  ? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
202
- : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} with cleanup warnings.`;
212
+ : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} due to cleanup blockers or warnings.`;
203
213
  ctx.ui.notify(`${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
204
214
  },
205
215
  });
@@ -8,6 +8,7 @@ import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { getAgentDir } from "@mariozechner/pi-coding-agent";
10
10
  import { isAbsolute, join, normalize } from "node:path";
11
+ import { getProjectId } from "./runtime.js";
11
12
 
12
13
  export const MODEL_FAMILIES = ["instant", "thinking", "pro"] as const;
13
14
  export type OracleModelFamily = (typeof MODEL_FAMILIES)[number];
@@ -258,6 +259,55 @@ const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecu
258
259
  const agentExtensionsDir = join(getAgentDir(), "extensions");
259
260
  const detectedChromeProfileName = detectDefaultChromeProfileName();
260
261
 
262
+ export interface OracleConfigLoadDetails {
263
+ agentDir: string;
264
+ agentConfigPath: string;
265
+ agentConfigExists: boolean;
266
+ projectConfigPath: string;
267
+ projectConfigExists: boolean;
268
+ effectiveAuthConfigPath: string;
269
+ effectiveAuthScope: "agent";
270
+ }
271
+
272
+ export function getOracleConfigLoadDetails(cwd: string): OracleConfigLoadDetails {
273
+ const agentDir = getAgentDir();
274
+ const projectRoot = getProjectId(cwd);
275
+ const agentConfigPath = join(agentDir, "extensions", "oracle.json");
276
+ const projectConfigPath = join(projectRoot, ".pi", "extensions", "oracle.json");
277
+ return {
278
+ agentDir,
279
+ agentConfigPath,
280
+ agentConfigExists: existsSync(agentConfigPath),
281
+ projectConfigPath,
282
+ projectConfigExists: existsSync(projectConfigPath),
283
+ effectiveAuthConfigPath: agentConfigPath,
284
+ effectiveAuthScope: "agent",
285
+ };
286
+ }
287
+
288
+ export function formatOracleAuthConfigRemediation(details: OracleConfigLoadDetails): string {
289
+ if (!details.projectConfigExists) {
290
+ return `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}.`;
291
+ }
292
+ return (
293
+ `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}. ` +
294
+ `Project overrides are also read from ${details.projectConfigPath}, but auth.* is loaded from ${details.effectiveAuthConfigPath}.`
295
+ );
296
+ }
297
+
298
+ export function formatOracleAuthConfigSummary(details: OracleConfigLoadDetails): string {
299
+ const lines = [
300
+ `Effective oracle auth config: ${details.effectiveAuthConfigPath} (agent dir: ${details.agentDir}${details.agentConfigExists ? "" : "; create this file to override auth.*"})`,
301
+ ];
302
+ if (details.projectConfigExists) {
303
+ lines.push(
304
+ `Project oracle config also loaded: ${details.projectConfigPath} ` +
305
+ `(project scope can override ${[...PROJECT_OVERRIDE_KEYS].join("/")} only; auth.* still comes from ${details.effectiveAuthConfigPath}).`,
306
+ );
307
+ }
308
+ return lines.join("\n");
309
+ }
310
+
261
311
  export const DEFAULT_CONFIG: OracleConfig = {
262
312
  defaults: {
263
313
  preset: "pro_extended",
@@ -514,7 +564,8 @@ function validateOracleConfig(value: unknown): OracleConfig {
514
564
  }
515
565
 
516
566
  export function loadOracleConfig(cwd: string): OracleConfig {
517
- const globalConfig = readJson(join(getAgentDir(), "extensions", "oracle.json"));
518
- const projectConfig = filterProjectConfig(readJson(join(cwd, ".pi", "extensions", "oracle.json")));
567
+ const details = getOracleConfigLoadDetails(cwd);
568
+ const globalConfig = readJson(details.agentConfigPath);
569
+ const projectConfig = filterProjectConfig(readJson(details.projectConfigPath));
519
570
  return validateOracleConfig(deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig));
520
571
  }