pi-oracle 0.3.4 → 0.5.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +27 -8
  3. package/docs/ORACLE_DESIGN.md +14 -8
  4. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
  5. package/extensions/oracle/index.ts +8 -1
  6. package/extensions/oracle/lib/commands.ts +25 -29
  7. package/extensions/oracle/lib/config.ts +56 -2
  8. package/extensions/oracle/lib/jobs.ts +134 -219
  9. package/extensions/oracle/lib/locks.ts +41 -209
  10. package/extensions/oracle/lib/poller.ts +38 -52
  11. package/extensions/oracle/lib/queue.ts +75 -112
  12. package/extensions/oracle/lib/runtime.ts +102 -19
  13. package/extensions/oracle/lib/tools.ts +663 -294
  14. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  15. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
  17. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
  18. package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
  19. package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
  20. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  21. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  23. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  24. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  25. package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
  26. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  28. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  30. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  31. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  32. package/extensions/oracle/worker/run-job.mjs +166 -274
  33. package/extensions/oracle/worker/state-locks.mjs +31 -216
  34. package/package.json +4 -3
  35. package/prompts/oracle.md +16 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-04-12
4
+
5
+ ### Added
6
+ - `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
7
+
8
+ ### Changed
9
+ - `/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
10
+ - `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
11
+ - `/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.*`
12
+ - `/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
13
+
14
+ ### Fixed
15
+ - `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
16
+ - oracle tool results now use consistent structured `details.job` / `details.error` payloads and preserve `isError` for structured failures through the tool-result hook
17
+ - `/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
18
+ - repeated oracle sanity runs now quiesce background pollers before isolated-state teardown so release verification no longer emits a noisy temp-lock ENOENT race
19
+
20
+ ## 0.4.0 - 2026-04-12
21
+
22
+ ### Added
23
+ - repeatable isolated local-extension `pi` validation guidance for oracle release verification, including smoke workflows that load the in-repo extension source directly
24
+ - persisted oracle lifecycle-event breadcrumbs plus richer detached-job observability in `oracle_submit`, `oracle_read`, poller wake-ups, and `/oracle-status`, including worker-log paths and last-event context
25
+ - shared worker/auth validation helpers, shared concurrency primitives, shared lifecycle reducers, and shared observability formatters to keep extension and worker behavior aligned
26
+ - extracted sanity-harness support and poller suites with typed helper scaffolding and repeated-run stability coverage
27
+
28
+ ### Changed
29
+ - oracle whole-repo archiving now excludes local tool state like `.pi/`, `.oracle-context/`, `.cursor/`, and `.scratchpad.md` by default while preserving explicitly requested paths
30
+ - lock/lease recovery, queue promotion, process identity handling, and lifecycle transitions now flow through shared helper modules instead of duplicated inline implementations
31
+ - worker/auth verification now leans on behaviorally tested helper modules plus dedicated `typecheck:worker-helpers` coverage instead of syntax checks and brittle source-string assertions alone
32
+ - release validation now expects isolated local-extension `pi` smoke tests and a stronger local oracle verification gate before shipping
33
+
34
+ ### Fixed
35
+ - archive input resolution now rejects symlink escapes outside the repo root and preserves safer repo-boundary handling for targeted archives
36
+ - hung `tar`, `zstd`, `cp`, and auth `agent-browser` subprocesses now time out and fail clearly instead of wedging archive, runtime-clone, or auth flows indefinitely
37
+ - cleanup warnings without a live worker no longer consume runtime/conversation capacity forever, while teardown still attempts lease release and preserves warnings for later triage
38
+ - detached oracle workers and poller flows now report clearer lifecycle breadcrumbs, wake-up settlement state, and failure context during fast-fail auth/bootstrap scenarios
39
+ - sanity-runner cleanup now retries transient temp-directory removal races, and the extracted harness is less timing-fragile and less `any`-driven than the previous monolithic runner
40
+
3
41
  ## 0.3.4 - 2026-04-11
4
42
 
5
43
  ### Changed
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 gather only minimal context and prefer a minimal archive instead of broad repo exploration. 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,15 @@ 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. Only archive README.md unless another file is clearly necessary.
71
+ ```
72
+
64
73
  ## High-level flow
65
74
 
66
75
  ```mermaid
67
76
  flowchart LR
68
- A["/oracle request"] --> B["Agent gathers repo context"]
77
+ A["/oracle request"] --> B["Agent preflights, then gathers only the needed repo context"]
69
78
  B --> C["oracle_submit builds archive"]
70
79
  C --> D["Detached worker starts isolated ChatGPT runtime"]
71
80
  D --> E["Archive + prompt sent to ChatGPT.com"]
@@ -82,9 +91,10 @@ User-facing commands:
82
91
  - `/oracle-auth` — sync ChatGPT cookies from your real Chrome profile into the isolated oracle auth profile
83
92
  - `/oracle-status [job-id]` — inspect job status
84
93
  - `/oracle-cancel [job-id]` — cancel queued or active job
85
- - `/oracle-clean <job-id|all>` — remove temp files for terminal jobs
94
+ - `/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
95
 
87
96
  Agent-facing tools:
97
+ - `oracle_preflight`
88
98
  - `oracle_submit`
89
99
  - `oracle_read`
90
100
  - `oracle_cancel`
@@ -143,6 +153,7 @@ Project config should only override safe, non-privileged settings.
143
153
  - Jobs can queue automatically if runtime capacity is full.
144
154
  - Completion delivery into `pi` is best-effort wake-up based.
145
155
  - If you miss the wake-up, use `oracle_read(jobId)` or `/oracle-status`.
156
+ - `/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
157
 
147
158
  ## Requirements
148
159
 
@@ -177,6 +188,12 @@ Project config should only override safe, non-privileged settings.
177
188
  - Use `/oracle-status [job-id]` or `oracle_read(jobId)`.
178
189
  - Results are still saved on disk even if the reminder turn does not land.
179
190
 
191
+ ### `/oracle-clean` refuses a terminal job right after completion
192
+
193
+ - This can happen during the short post-send retention grace window after a wake-up was sent.
194
+ - The command now returns a `Retry after ...` timestamp when that guard is active.
195
+ - Wait until that time, then rerun `/oracle-clean [job-id|all]`.
196
+
180
197
  ### `agent-browser`, `tar`, or `zstd` is missing
181
198
 
182
199
  - Install the missing local dependency and rerun the command.
@@ -195,6 +212,7 @@ Project config should only override safe, non-privileged settings.
195
212
 
196
213
  - `docs/ORACLE_DESIGN.md` — architecture, lifecycle, queueing, persistence, presets, and recovery behavior
197
214
  - `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
215
+ - `docs/ORACLE_ISOLATED_PI_VALIDATION.md` — repeatable isolated `pi` session smoke test for local-extension validation
198
216
 
199
217
  ## Privacy / local data
200
218
 
@@ -210,6 +228,7 @@ Review the code and design docs before using it with sensitive material.
210
228
  ```bash
211
229
  npm run check:oracle-extension
212
230
  npm run typecheck
231
+ npm run typecheck:worker-helpers
213
232
  npm run sanity:oracle
214
233
  npm run pack:check
215
234
  # conventional local gate
@@ -79,6 +79,9 @@ The extension now follows the current `pi` session lifecycle model:
79
79
 
80
80
  ### Tools
81
81
 
82
+ - `oracle_preflight`
83
+ - lightweight agent-facing readiness check for persisted-session and local oracle prerequisites
84
+ - intended to run before expensive `/oracle` context gathering
82
85
  - `oracle_submit`
83
86
  - low-level agent-facing dispatch tool
84
87
  - creates archive and launches a detached worker
@@ -97,12 +100,15 @@ It expands through the prompt-template path so pi can apply its native queueing
97
100
 
98
101
  Instead it instructs the agent to:
99
102
 
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)
103
+ 1. call `oracle_preflight` immediately
104
+ 2. stop right away if preflight reports the session or local oracle setup is not ready
105
+ 3. understand whether the request is explicitly narrow or genuinely broad
106
+ 4. gather only the smallest repo context needed to submit well
107
+ 5. if the request is narrow, prefer a minimal targeted archive and dispatch as soon as enough context is in hand
108
+ 6. if the request is broad/repo-wide, gather broader context and usually archive `.`
109
+ 7. craft the oracle prompt
110
+ 8. call `oracle_submit`
111
+ 9. stop and wait for the completion wake-up (best-effort; durable oracle response/artifact state is already persisted outside session history)
106
112
 
107
113
  ### `/oracle-auth`
108
114
 
@@ -133,7 +139,7 @@ The authenticated seed profile remains the source of truth for future oracle run
133
139
 
134
140
  ### `oracle_submit`
135
141
 
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.
142
+ 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.
137
143
 
138
144
  1. resolve the preset (submit-time or config default) into an execution snapshot
139
145
  2. resolve optional `followUpJobId` into a prior `chatUrl` and `conversationId`
@@ -262,7 +268,7 @@ Long-run hygiene is intentionally conservative:
262
268
 
263
269
  - runtime profiles, runtime leases, and conversation leases are cleaned immediately as part of worker/command cleanup paths
264
270
  - 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
271
+ - `/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
272
  - stale lock directories are swept before reconcile maintenance
267
273
  - old auth `.staging-*` profiles are swept during `/oracle-auth` startup when the auth browser session is not still active
268
274
  - terminal job directories are retained for inspection, then pruned later based on configurable retention windows
@@ -0,0 +1,276 @@
1
+ # Oracle isolated `pi` validation
2
+
3
+ This document describes the repeatable pre-commit smoke test for validating `pi-oracle` through isolated `pi` agent sessions that load the local extension source.
4
+
5
+ Use this workflow for code changes when you need end-to-end evidence beyond `npm test`.
6
+
7
+ ## What this validates
8
+
9
+ - the local extension can be loaded directly by isolated `pi` sessions
10
+ - whole-repo `oracle_submit` archive creation excludes local tool state by default
11
+ - targeted archive inputs cannot escape the repo through symlinked paths
12
+ - the exercised `pi` agents can provide candid feedback about tool clarity or clunkiness
13
+
14
+ ## Why this workflow is isolated
15
+
16
+ The test intentionally uses separate directories for:
17
+
18
+ - `PI_CODING_AGENT_DIR`
19
+ - `--session-dir`
20
+ - `PI_ORACLE_JOBS_DIR`
21
+
22
+ That keeps the validation run from reusing your normal `pi` agent state.
23
+
24
+ The extension is loaded from the local checkout with:
25
+
26
+ ```bash
27
+ pi --no-extensions -e "$REPO/extensions/oracle/index.ts"
28
+ ```
29
+
30
+ That ensures the session is exercising the in-repo code, not a globally installed package.
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
+
43
+ ## Preset requirement
44
+
45
+ Use either:
46
+
47
+ - `instant`
48
+ - `thinking_light`
49
+
50
+ The examples below use `instant` because it is the fastest smoke-test preset.
51
+
52
+ ## Prerequisites
53
+
54
+ - `pi` installed locally
55
+ - `tmux` installed locally
56
+ - run from the repository root
57
+
58
+ ## Repeatable smoke test
59
+
60
+ ```bash
61
+ set -euo pipefail
62
+
63
+ REPO="$PWD"
64
+ TEST_ROOT="/tmp/pi-oracle-isolated-tests-$$"
65
+
66
+ TEST1_AGENT="$TEST_ROOT/agent1"
67
+ TEST1_SESSIONS="$TEST_ROOT/sessions1"
68
+ TEST1_JOBS="$TEST_ROOT/jobs1"
69
+ TEST2_AGENT="$TEST_ROOT/agent2"
70
+ TEST2_SESSIONS="$TEST_ROOT/sessions2"
71
+ TEST2_JOBS="$TEST_ROOT/jobs2"
72
+
73
+ FIXTURE="$TEST_ROOT/symlink-fixture"
74
+ OUTSIDE="$TEST_ROOT/outside"
75
+
76
+ SESSION1="pi-oracle-test1"
77
+ SESSION2="pi-oracle-test2"
78
+
79
+ mkdir -p \
80
+ "$TEST1_AGENT" "$TEST1_SESSIONS" "$TEST1_JOBS" \
81
+ "$TEST2_AGENT" "$TEST2_SESSIONS" "$TEST2_JOBS" \
82
+ "$FIXTURE" "$OUTSIDE"
83
+
84
+ mkdir -p "$TEST1_AGENT/extensions/oracle-auth-seed-profile"
85
+
86
+ echo 'secret' > "$OUTSIDE/secret.txt"
87
+ ln -s "$OUTSIDE" "$FIXTURE/linked-outside"
88
+
89
+ PROMPT1='Call oracle_submit directly with prompt "Sanity test for archive exclusions. Reply with OK." files ["."] and preset "instant". Do not use bash. After the tool returns, summarize the outcome in 3 bullets including the job id/status, and give one sentence of candid feedback on whether the oracle tool behavior feels clear or clunky.'
90
+ PROMPT2='Call oracle_submit directly with prompt "Sanity test for symlink escape rejection." files ["linked-outside/secret.txt"] and preset "instant". Do not use bash. After the tool returns, summarize the outcome in 3 bullets and give one sentence of candid feedback on whether the oracle tool behavior feels clear or clunky.'
91
+
92
+ cleanup() {
93
+ tmux kill-session -t "$SESSION1" 2>/dev/null || true
94
+ tmux kill-session -t "$SESSION2" 2>/dev/null || true
95
+ }
96
+ trap cleanup EXIT
97
+ cleanup
98
+
99
+ TMUX_CMD1="cd '$REPO' && env PI_CODING_AGENT_DIR='$TEST1_AGENT' PI_ORACLE_JOBS_DIR='$TEST1_JOBS' PATH='$PATH' pi --session-dir '$TEST1_SESSIONS' --no-extensions -e '$REPO/extensions/oracle/index.ts'"
100
+ tmux new-session -d -s "$SESSION1" "$TMUX_CMD1"
101
+ sleep 8
102
+ tmux send-keys -t "$SESSION1":0.0 "$PROMPT1" Enter
103
+ sleep 35
104
+
105
+ echo '--- pane:test1'
106
+ tmux capture-pane -p -S -220 -t "$SESSION1":0.0 | tail -n 160
107
+
108
+ JOB_DIR1="$(find "$TEST1_JOBS" -maxdepth 1 -type d -name 'oracle-*' | sort | tail -n 1 || true)"
109
+ echo "--- latest job dir:test1 ${JOB_DIR1:-<none>}"
110
+
111
+ if [ -n "${JOB_DIR1:-}" ] && [ -f "$JOB_DIR1/job.json" ]; then
112
+ ARCHIVE1="$(python3 - <<'PY' "$JOB_DIR1/job.json"
113
+ import json,sys
114
+ with open(sys.argv[1]) as f:
115
+ print(json.load(f)['archivePath'])
116
+ PY
117
+ )"
118
+ echo "--- archive:test1 $ARCHIVE1"
119
+ tar --zstd -tf "$ARCHIVE1" | head -n 80
120
+ LIST="$(mktemp)"
121
+ tar --zstd -tf "$ARCHIVE1" > "$LIST"
122
+ for path in .pi/settings.json .oracle-context .cursor .scratchpad.md README.md; do
123
+ if grep -E -q "^${path}$|^${path}/" "$LIST"; then
124
+ echo "FOUND $path"
125
+ else
126
+ echo "MISSING $path"
127
+ fi
128
+ done
129
+ rm -f "$LIST"
130
+ fi
131
+
132
+ TMUX_CMD2="cd '$FIXTURE' && env PI_CODING_AGENT_DIR='$TEST2_AGENT' PI_ORACLE_JOBS_DIR='$TEST2_JOBS' PATH='$PATH' pi --session-dir '$TEST2_SESSIONS' --no-extensions -e '$REPO/extensions/oracle/index.ts'"
133
+ tmux new-session -d -s "$SESSION2" "$TMUX_CMD2"
134
+ sleep 8
135
+ tmux send-keys -t "$SESSION2":0.0 "$PROMPT2" Enter
136
+ sleep 25
137
+
138
+ echo '--- pane:test2'
139
+ tmux capture-pane -p -S -220 -t "$SESSION2":0.0 | tail -n 160
140
+
141
+ echo '--- jobs created:test2'
142
+ find "$TEST2_JOBS" -maxdepth 1 -type d -name 'oracle-*' | sort || true
143
+
144
+ echo "TEST_ROOT=$TEST_ROOT"
145
+ ```
146
+
147
+ ## Expected results
148
+
149
+ ### Test 1: whole-repo archive exclusions
150
+
151
+ Expected behavior:
152
+
153
+ - the isolated `pi` session loads the local extension successfully
154
+ - `oracle_submit` creates a job and an archive path under the isolated jobs dir
155
+ - the archive should exclude:
156
+ - `.pi/`
157
+ - `.oracle-context/`
158
+ - `.cursor/`
159
+ - `.scratchpad.md`
160
+ - the archive should still include normal repo files such as `README.md`
161
+
162
+ Notes:
163
+
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
167
+
168
+ ### Test 2: symlink escape rejection
169
+
170
+ Expected behavior:
171
+
172
+ - `oracle_submit` rejects `linked-outside/secret.txt`
173
+ - the error should say the archive input must resolve inside the project cwd without symlink escapes
174
+ - no oracle job directory should be created for the rejected submit
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
+
189
+ ## Additional failure-mode smoke tests
190
+
191
+ ### `/oracle-auth` should fail fast when `agent-browser` hangs
192
+
193
+ Use this when validating timeout hardening around auth/bootstrap browser commands.
194
+
195
+ ```bash
196
+ set -euo pipefail
197
+
198
+ REPO="$PWD"
199
+ TEST_ROOT="/tmp/pi-oracle-auth-timeout-$$"
200
+ AGENT_DIR="$TEST_ROOT/agent"
201
+ SESSION_DIR="$TEST_ROOT/sessions"
202
+ JOBS_DIR="$TEST_ROOT/jobs"
203
+ FAKE_BROWSER="$TEST_ROOT/agent-browser"
204
+ SESSION_NAME="pi-oracle-auth-timeout"
205
+
206
+ mkdir -p "$AGENT_DIR/extensions" "$SESSION_DIR" "$JOBS_DIR"
207
+
208
+ cat > "$AGENT_DIR/extensions/oracle.json" <<JSON
209
+ {
210
+ "auth": {
211
+ "chromeCookiePath": "$TEST_ROOT/missing-cookies.sqlite"
212
+ }
213
+ }
214
+ JSON
215
+
216
+ cat > "$FAKE_BROWSER" <<'SH'
217
+ #!/bin/sh
218
+ trap 'exit 0' TERM INT
219
+ while :; do sleep 1; done
220
+ SH
221
+ chmod +x "$FAKE_BROWSER"
222
+
223
+ cleanup() {
224
+ tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
225
+ }
226
+ trap 'cleanup; rm -rf "$TEST_ROOT"' EXIT
227
+ cleanup
228
+
229
+ TMUX_CMD="cd '$REPO' && env PI_CODING_AGENT_DIR='$AGENT_DIR' PI_ORACLE_JOBS_DIR='$JOBS_DIR' AGENT_BROWSER_PATH='$FAKE_BROWSER' PI_ORACLE_AUTH_AGENT_BROWSER_TIMEOUT_MS='250' PI_ORACLE_AUTH_CLOSE_TIMEOUT_MS='250' PI_ORACLE_AUTH_KILL_GRACE_MS='100' PATH='$PATH' pi --session-dir '$SESSION_DIR' --no-extensions -e '$REPO/extensions/oracle/index.ts'"
230
+
231
+ tmux new-session -d -s "$SESSION_NAME" "$TMUX_CMD"
232
+ sleep 8
233
+ tmux send-keys -t "$SESSION_NAME":0.0 '/oracle-auth' Enter
234
+ sleep 12
235
+
236
+ tmux capture-pane -p -S -220 -t "$SESSION_NAME":0.0 | tail -n 140
237
+ ```
238
+
239
+ Expected behavior:
240
+
241
+ - the isolated `pi` session loads the local extension successfully
242
+ - `/oracle-auth` returns with an error instead of hanging indefinitely
243
+ - the output should mention the missing ChatGPT session-token cookies or the configured cookie source problem
244
+ - the session should remain usable after the command failure
245
+
246
+ ## Switching to `thinking_light`
247
+
248
+ To run the same smoke test with `thinking_light`, change both prompts from:
249
+
250
+ ```text
251
+ preset "instant"
252
+ ```
253
+
254
+ to:
255
+
256
+ ```text
257
+ preset "thinking_light"
258
+ ```
259
+
260
+ ## Cleanup
261
+
262
+ The snippet already kills the temporary `tmux` sessions on exit.
263
+
264
+ To remove the temporary files after inspection:
265
+
266
+ ```bash
267
+ rm -rf "$TEST_ROOT"
268
+ ```
269
+
270
+ ## Minimum pre-commit evidence
271
+
272
+ Before committing code changes, keep evidence for:
273
+
274
+ - `npm test` passing
275
+ - isolated `pi` session validation using this workflow
276
+ - any agent feedback gathered during the isolated run if it exposed clunky or unclear behavior
@@ -1,3 +1,8 @@
1
+ // Purpose: Register the oracle extension, wire commands/tools/workers, and manage per-session background maintenance.
2
+ // Responsibilities: Bootstrap oracle commands and tools, start or stop polling, and surface startup/config availability in the pi session UI.
3
+ // Scope: Extension entrypoint only; lifecycle mutation lives in lib modules and browser execution lives in worker scripts.
4
+ // Usage: Loaded by pi as the extension module declared in package.json.
5
+ // Invariants/Assumptions: Oracle only runs against persisted sessions, and startup maintenance should be best-effort without breaking session initialization.
1
6
  import { fileURLToPath } from "node:url";
2
7
  import { dirname, join } from "node:path";
3
8
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
@@ -42,7 +47,9 @@ export default function oracleExtension(pi: ExtensionAPI) {
42
47
 
43
48
  const config = loadOracleConfig(ctx.cwd);
44
49
  void runStartupMaintenance(ctx).catch((error) => {
45
- console.error("Oracle startup maintenance failed:", error);
50
+ const message = `Oracle startup maintenance failed: ${error instanceof Error ? error.message : String(error)}`;
51
+ console.error(message);
52
+ ctx.ui.notify(message, "warning");
46
53
  });
47
54
  startPoller(pi, ctx, config.poller.intervalMs, workerPath);
48
55
  refreshOracleStatus(ctx);
@@ -1,8 +1,16 @@
1
+ // Purpose: Register slash commands for oracle auth/bootstrap, status inspection, cancellation, and cleanup.
2
+ // Responsibilities: Bridge command handlers to shared oracle lifecycle helpers, surface consistent summaries, and coordinate follow-up queue advancement.
3
+ // Scope: Command-facing orchestration only; durable lifecycle mutations live in jobs/runtime/tools modules and browser execution stays in worker scripts.
4
+ // Usage: Imported by the oracle extension entrypoint to register /oracle-* commands with pi.
5
+ // Invariants/Assumptions: Commands operate on persisted project-scoped jobs and rely on shared observability formatting for detached-state clarity.
1
6
  import { spawn } from "node:child_process";
7
+ import { existsSync } from "node:fs";
2
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
3
- import { loadOracleConfig } from "./config.js";
9
+ import { formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
10
+ import { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig } from "./config.js";
4
11
  import {
5
12
  cancelOracleJob,
13
+ getJobDir,
6
14
  isOpenOracleJob,
7
15
  isTerminalOracleJob,
8
16
  listJobsForCwd,
@@ -22,30 +30,12 @@ function summarizeJob(jobId: string): string {
22
30
  const job = readJob(jobId);
23
31
  if (!job) return `Oracle job ${jobId} not found.`;
24
32
 
25
- const queuePosition = job.status === "queued" ? getQueuePosition(job.id) : undefined;
26
- return [
27
- `job: ${job.id}`,
28
- `status: ${job.status}`,
29
- `phase: ${job.phase}`,
30
- `created: ${job.createdAt}`,
31
- job.queuedAt ? `queued: ${job.queuedAt}` : undefined,
32
- job.submittedAt ? `submitted: ${job.submittedAt}` : undefined,
33
- queuePosition ? `queue-position: ${queuePosition.position} of ${queuePosition.depth} global` : undefined,
34
- `project: ${job.projectId}`,
35
- `session: ${job.sessionId}`,
36
- job.completedAt ? `completed: ${job.completedAt}` : undefined,
37
- job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
38
- job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
39
- job.conversationId ? `conversation: ${job.conversationId}` : undefined,
40
- job.responsePath ? `response: ${job.responsePath}` : undefined,
41
- job.responseFormat ? `response-format: ${job.responseFormat}` : undefined,
42
- typeof job.artifactFailureCount === "number" ? `artifact-failures: ${job.artifactFailureCount}` : undefined,
43
- job.lastCleanupAt ? `last-cleanup: ${job.lastCleanupAt}` : undefined,
44
- job.cleanupWarnings?.length ? `cleanup-warnings: ${job.cleanupWarnings.join(" | ")}` : undefined,
45
- job.error ? `error: ${job.error}` : undefined,
46
- ]
47
- .filter(Boolean)
48
- .join("\n");
33
+ const responseAvailable = Boolean(job.responsePath && existsSync(job.responsePath));
34
+ return formatOracleJobSummary(job, {
35
+ queuePosition: job.status === "queued" ? getQueuePosition(job.id) : undefined,
36
+ artifactsPath: `${getJobDir(job.id)}/artifacts`,
37
+ responseAvailable,
38
+ });
49
39
  }
50
40
 
51
41
  function getLatestJobId(cwd: string): string | undefined {
@@ -60,6 +50,12 @@ function readScopedJob(jobId: string, cwd: string) {
60
50
 
61
51
  async function runAuthBootstrap(authWorkerPath: string, cwd: string): Promise<string> {
62
52
  const config = loadOracleConfig(cwd);
53
+ const configLoad = getOracleConfigLoadDetails(cwd);
54
+ const authConfigGuidance = {
55
+ ...configLoad,
56
+ remediation: formatOracleAuthConfigRemediation(configLoad),
57
+ summary: formatOracleAuthConfigSummary(configLoad),
58
+ };
63
59
  try {
64
60
  await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_auth", cwd }, async () => {
65
61
  await reconcileStaleOracleJobs();
@@ -70,7 +66,7 @@ async function runAuthBootstrap(authWorkerPath: string, cwd: string): Promise<st
70
66
  }
71
67
 
72
68
  return await new Promise<string>((resolve, reject) => {
73
- const child = spawn(process.execPath, [authWorkerPath, JSON.stringify(config)], {
69
+ const child = spawn(process.execPath, [authWorkerPath, JSON.stringify({ config, configLoad: authConfigGuidance })], {
74
70
  cwd,
75
71
  stdio: ["ignore", "pipe", "pipe"],
76
72
  });
@@ -164,7 +160,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
164
160
  });
165
161
 
166
162
  pi.registerCommand("oracle-clean", {
167
- description: "Remove oracle temp files for a job or all project jobs",
163
+ description: "Remove oracle temp files for terminal jobs; recently woken jobs may stay retained briefly",
168
164
  handler: async (args, ctx: ExtensionCommandContext) => {
169
165
  const target = args.trim();
170
166
  if (!target) {
@@ -209,10 +205,10 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
209
205
  }
210
206
 
211
207
  refreshOracleStatus(ctx);
212
- const warningSuffix = cleanupWarnings.length > 0 ? ` Cleanup warnings:\n${cleanupWarnings.join("\n")}` : "";
208
+ const warningSuffix = cleanupWarnings.length > 0 ? ` Cleanup blockers/warnings:\n${cleanupWarnings.join("\n")}` : "";
213
209
  const removalSummary = removedCount === jobs.length
214
210
  ? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
215
- : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} with cleanup warnings.`;
211
+ : `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} due to cleanup blockers or warnings.`;
216
212
  ctx.ui.notify(`${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
217
213
  },
218
214
  });
@@ -1,3 +1,8 @@
1
+ // Purpose: Define oracle configuration schema, defaults, preset selection, and local config loading behavior.
2
+ // Responsibilities: Normalize preset ids, load extension config from disk, expose default browser/auth/runtime settings, and validate config shape.
3
+ // Scope: Configuration and preset resolution only; runtime/job execution stays in sibling oracle modules.
4
+ // Usage: Imported by oracle tools, commands, runtime helpers, and sanity tests when config or preset resolution is required.
5
+ // Invariants/Assumptions: Preset ids remain the canonical model-selection contract and config loading must fail clearly on invalid user overrides.
1
6
  import { execFileSync } from "node:child_process";
2
7
  import { existsSync, readFileSync } from "node:fs";
3
8
  import { homedir } from "node:os";
@@ -253,6 +258,54 @@ const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecu
253
258
  const agentExtensionsDir = join(getAgentDir(), "extensions");
254
259
  const detectedChromeProfileName = detectDefaultChromeProfileName();
255
260
 
261
+ export interface OracleConfigLoadDetails {
262
+ agentDir: string;
263
+ agentConfigPath: string;
264
+ agentConfigExists: boolean;
265
+ projectConfigPath: string;
266
+ projectConfigExists: boolean;
267
+ effectiveAuthConfigPath: string;
268
+ effectiveAuthScope: "agent";
269
+ }
270
+
271
+ export function getOracleConfigLoadDetails(cwd: string): OracleConfigLoadDetails {
272
+ const agentDir = getAgentDir();
273
+ const agentConfigPath = join(agentDir, "extensions", "oracle.json");
274
+ const projectConfigPath = join(cwd, ".pi", "extensions", "oracle.json");
275
+ return {
276
+ agentDir,
277
+ agentConfigPath,
278
+ agentConfigExists: existsSync(agentConfigPath),
279
+ projectConfigPath,
280
+ projectConfigExists: existsSync(projectConfigPath),
281
+ effectiveAuthConfigPath: agentConfigPath,
282
+ effectiveAuthScope: "agent",
283
+ };
284
+ }
285
+
286
+ export function formatOracleAuthConfigRemediation(details: OracleConfigLoadDetails): string {
287
+ if (!details.projectConfigExists) {
288
+ return `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}.`;
289
+ }
290
+ return (
291
+ `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}. ` +
292
+ `Project overrides are also read from ${details.projectConfigPath}, but auth.* is loaded from ${details.effectiveAuthConfigPath}.`
293
+ );
294
+ }
295
+
296
+ export function formatOracleAuthConfigSummary(details: OracleConfigLoadDetails): string {
297
+ const lines = [
298
+ `Effective oracle auth config: ${details.effectiveAuthConfigPath} (agent dir: ${details.agentDir}${details.agentConfigExists ? "" : "; create this file to override auth.*"})`,
299
+ ];
300
+ if (details.projectConfigExists) {
301
+ lines.push(
302
+ `Project oracle config also loaded: ${details.projectConfigPath} ` +
303
+ `(project scope can override ${[...PROJECT_OVERRIDE_KEYS].join("/")} only; auth.* still comes from ${details.effectiveAuthConfigPath}).`,
304
+ );
305
+ }
306
+ return lines.join("\n");
307
+ }
308
+
256
309
  export const DEFAULT_CONFIG: OracleConfig = {
257
310
  defaults: {
258
311
  preset: "pro_extended",
@@ -509,7 +562,8 @@ function validateOracleConfig(value: unknown): OracleConfig {
509
562
  }
510
563
 
511
564
  export function loadOracleConfig(cwd: string): OracleConfig {
512
- const globalConfig = readJson(join(getAgentDir(), "extensions", "oracle.json"));
513
- const projectConfig = filterProjectConfig(readJson(join(cwd, ".pi", "extensions", "oracle.json")));
565
+ const details = getOracleConfigLoadDetails(cwd);
566
+ const globalConfig = readJson(details.agentConfigPath);
567
+ const projectConfig = filterProjectConfig(readJson(details.projectConfigPath));
514
568
  return validateOracleConfig(deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig));
515
569
  }