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 +33 -0
- package/README.md +43 -12
- package/docs/ORACLE_DESIGN.md +32 -16
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +28 -1
- package/extensions/oracle/index.ts +1 -1
- package/extensions/oracle/lib/auth.ts +50 -0
- package/extensions/oracle/lib/commands.ts +57 -47
- package/extensions/oracle/lib/config.ts +53 -2
- package/extensions/oracle/lib/jobs.ts +31 -5
- package/extensions/oracle/lib/poller.ts +33 -4
- package/extensions/oracle/lib/runtime.ts +171 -7
- package/extensions/oracle/lib/tools.ts +726 -253
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +1 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +13 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +2 -1
- package/extensions/oracle/shared/job-observability-helpers.mjs +28 -10
- package/extensions/oracle/worker/auth-bootstrap.mjs +49 -4
- package/extensions/oracle/worker/auth-flow-helpers.mjs +1 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +106 -41
- package/extensions/oracle/worker/run-job.mjs +17 -13
- package/package.json +6 -2
- package/prompts/oracle-followup.md +48 -0
- package/prompts/oracle.md +18 -11
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.
|
|
46
|
-
2. Make sure
|
|
47
|
-
3.
|
|
48
|
-
4.
|
|
49
|
-
5. Run `/oracle
|
|
50
|
-
6.
|
|
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
|
|
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-
|
|
84
|
-
- `/oracle-
|
|
85
|
-
- `/oracle-
|
|
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 `
|
|
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-
|
|
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.
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -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
|
|
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.
|
|
101
|
-
2.
|
|
102
|
-
3.
|
|
103
|
-
4.
|
|
104
|
-
5.
|
|
105
|
-
6.
|
|
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 `
|
|
471
|
-
- manual `oracle_read
|
|
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
|
-
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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-
|
|
122
|
-
description: "
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
|
518
|
-
const
|
|
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
|
}
|