pi-oracle 0.3.4 → 0.4.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 (33) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -0
  3. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +249 -0
  4. package/extensions/oracle/index.ts +8 -1
  5. package/extensions/oracle/lib/commands.ts +11 -24
  6. package/extensions/oracle/lib/config.ts +5 -0
  7. package/extensions/oracle/lib/jobs.ts +117 -217
  8. package/extensions/oracle/lib/locks.ts +41 -209
  9. package/extensions/oracle/lib/poller.ts +14 -51
  10. package/extensions/oracle/lib/queue.ts +75 -112
  11. package/extensions/oracle/lib/runtime.ts +60 -14
  12. package/extensions/oracle/lib/tools.ts +66 -65
  13. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  14. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  15. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +130 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +377 -0
  17. package/extensions/oracle/shared/job-observability-helpers.d.mts +59 -0
  18. package/extensions/oracle/shared/job-observability-helpers.mjs +143 -0
  19. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  20. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  21. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  23. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  24. package/extensions/oracle/worker/auth-bootstrap.mjs +76 -130
  25. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  26. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  28. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  30. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  31. package/extensions/oracle/worker/run-job.mjs +166 -274
  32. package/extensions/oracle/worker/state-locks.mjs +31 -216
  33. package/package.json +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-04-12
4
+
5
+ ### Added
6
+ - repeatable isolated local-extension `pi` validation guidance for oracle release verification, including smoke workflows that load the in-repo extension source directly
7
+ - 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
8
+ - shared worker/auth validation helpers, shared concurrency primitives, shared lifecycle reducers, and shared observability formatters to keep extension and worker behavior aligned
9
+ - extracted sanity-harness support and poller suites with typed helper scaffolding and repeated-run stability coverage
10
+
11
+ ### Changed
12
+ - oracle whole-repo archiving now excludes local tool state like `.pi/`, `.oracle-context/`, `.cursor/`, and `.scratchpad.md` by default while preserving explicitly requested paths
13
+ - lock/lease recovery, queue promotion, process identity handling, and lifecycle transitions now flow through shared helper modules instead of duplicated inline implementations
14
+ - 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
15
+ - release validation now expects isolated local-extension `pi` smoke tests and a stronger local oracle verification gate before shipping
16
+
17
+ ### Fixed
18
+ - archive input resolution now rejects symlink escapes outside the repo root and preserves safer repo-boundary handling for targeted archives
19
+ - 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
20
+ - 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
21
+ - detached oracle workers and poller flows now report clearer lifecycle breadcrumbs, wake-up settlement state, and failure context during fast-fail auth/bootstrap scenarios
22
+ - 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
23
+
3
24
  ## 0.3.4 - 2026-04-11
4
25
 
5
26
  ### Changed
package/README.md CHANGED
@@ -195,6 +195,7 @@ Project config should only override safe, non-privileged settings.
195
195
 
196
196
  - `docs/ORACLE_DESIGN.md` — architecture, lifecycle, queueing, persistence, presets, and recovery behavior
197
197
  - `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
198
+ - `docs/ORACLE_ISOLATED_PI_VALIDATION.md` — repeatable isolated `pi` session smoke test for local-extension validation
198
199
 
199
200
  ## Privacy / local data
200
201
 
@@ -210,6 +211,7 @@ Review the code and design docs before using it with sensitive material.
210
211
  ```bash
211
212
  npm run check:oracle-extension
212
213
  npm run typecheck
214
+ npm run typecheck:worker-helpers
213
215
  npm run sanity:oracle
214
216
  npm run pack:check
215
217
  # conventional local gate
@@ -0,0 +1,249 @@
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
+ ## Preset requirement
33
+
34
+ Use either:
35
+
36
+ - `instant`
37
+ - `thinking_light`
38
+
39
+ The examples below use `instant` because it is the fastest smoke-test preset.
40
+
41
+ ## Prerequisites
42
+
43
+ - `pi` installed locally
44
+ - `tmux` installed locally
45
+ - run from the repository root
46
+
47
+ ## Repeatable smoke test
48
+
49
+ ```bash
50
+ set -euo pipefail
51
+
52
+ REPO="$PWD"
53
+ TEST_ROOT="/tmp/pi-oracle-isolated-tests-$$"
54
+
55
+ TEST1_AGENT="$TEST_ROOT/agent1"
56
+ TEST1_SESSIONS="$TEST_ROOT/sessions1"
57
+ TEST1_JOBS="$TEST_ROOT/jobs1"
58
+ TEST2_AGENT="$TEST_ROOT/agent2"
59
+ TEST2_SESSIONS="$TEST_ROOT/sessions2"
60
+ TEST2_JOBS="$TEST_ROOT/jobs2"
61
+
62
+ FIXTURE="$TEST_ROOT/symlink-fixture"
63
+ OUTSIDE="$TEST_ROOT/outside"
64
+
65
+ SESSION1="pi-oracle-test1"
66
+ SESSION2="pi-oracle-test2"
67
+
68
+ mkdir -p \
69
+ "$TEST1_AGENT" "$TEST1_SESSIONS" "$TEST1_JOBS" \
70
+ "$TEST2_AGENT" "$TEST2_SESSIONS" "$TEST2_JOBS" \
71
+ "$FIXTURE" "$OUTSIDE"
72
+
73
+ echo 'secret' > "$OUTSIDE/secret.txt"
74
+ ln -s "$OUTSIDE" "$FIXTURE/linked-outside"
75
+
76
+ 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.'
77
+ 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.'
78
+
79
+ cleanup() {
80
+ tmux kill-session -t "$SESSION1" 2>/dev/null || true
81
+ tmux kill-session -t "$SESSION2" 2>/dev/null || true
82
+ }
83
+ trap cleanup EXIT
84
+ cleanup
85
+
86
+ 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'"
87
+ tmux new-session -d -s "$SESSION1" "$TMUX_CMD1"
88
+ sleep 8
89
+ tmux send-keys -t "$SESSION1":0.0 "$PROMPT1" Enter
90
+ sleep 35
91
+
92
+ echo '--- pane:test1'
93
+ tmux capture-pane -p -S -220 -t "$SESSION1":0.0 | tail -n 160
94
+
95
+ JOB_DIR1="$(find "$TEST1_JOBS" -maxdepth 1 -type d -name 'oracle-*' | sort | tail -n 1 || true)"
96
+ echo "--- latest job dir:test1 ${JOB_DIR1:-<none>}"
97
+
98
+ if [ -n "${JOB_DIR1:-}" ] && [ -f "$JOB_DIR1/job.json" ]; then
99
+ ARCHIVE1="$(python3 - <<'PY' "$JOB_DIR1/job.json"
100
+ import json,sys
101
+ with open(sys.argv[1]) as f:
102
+ print(json.load(f)['archivePath'])
103
+ PY
104
+ )"
105
+ echo "--- archive:test1 $ARCHIVE1"
106
+ tar --zstd -tf "$ARCHIVE1" | head -n 80
107
+ LIST="$(mktemp)"
108
+ tar --zstd -tf "$ARCHIVE1" > "$LIST"
109
+ for path in .pi/settings.json .oracle-context .cursor .scratchpad.md README.md; do
110
+ if grep -E -q "^${path}$|^${path}/" "$LIST"; then
111
+ echo "FOUND $path"
112
+ else
113
+ echo "MISSING $path"
114
+ fi
115
+ done
116
+ rm -f "$LIST"
117
+ fi
118
+
119
+ 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'"
120
+ tmux new-session -d -s "$SESSION2" "$TMUX_CMD2"
121
+ sleep 8
122
+ tmux send-keys -t "$SESSION2":0.0 "$PROMPT2" Enter
123
+ sleep 25
124
+
125
+ echo '--- pane:test2'
126
+ tmux capture-pane -p -S -220 -t "$SESSION2":0.0 | tail -n 160
127
+
128
+ echo '--- jobs created:test2'
129
+ find "$TEST2_JOBS" -maxdepth 1 -type d -name 'oracle-*' | sort || true
130
+
131
+ echo "TEST_ROOT=$TEST_ROOT"
132
+ ```
133
+
134
+ ## Expected results
135
+
136
+ ### Test 1: whole-repo archive exclusions
137
+
138
+ Expected behavior:
139
+
140
+ - the isolated `pi` session loads the local extension successfully
141
+ - `oracle_submit` creates a job and an archive path under the isolated jobs dir
142
+ - the archive should exclude:
143
+ - `.pi/`
144
+ - `.oracle-context/`
145
+ - `.cursor/`
146
+ - `.scratchpad.md`
147
+ - the archive should still include normal repo files such as `README.md`
148
+
149
+ Notes:
150
+
151
+ - 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
153
+
154
+ ### Test 2: symlink escape rejection
155
+
156
+ Expected behavior:
157
+
158
+ - `oracle_submit` rejects `linked-outside/secret.txt`
159
+ - the error should say the archive input must resolve inside the project cwd without symlink escapes
160
+ - no oracle job directory should be created for the rejected submit
161
+
162
+ ## Additional failure-mode smoke tests
163
+
164
+ ### `/oracle-auth` should fail fast when `agent-browser` hangs
165
+
166
+ Use this when validating timeout hardening around auth/bootstrap browser commands.
167
+
168
+ ```bash
169
+ set -euo pipefail
170
+
171
+ REPO="$PWD"
172
+ TEST_ROOT="/tmp/pi-oracle-auth-timeout-$$"
173
+ AGENT_DIR="$TEST_ROOT/agent"
174
+ SESSION_DIR="$TEST_ROOT/sessions"
175
+ JOBS_DIR="$TEST_ROOT/jobs"
176
+ FAKE_BROWSER="$TEST_ROOT/agent-browser"
177
+ SESSION_NAME="pi-oracle-auth-timeout"
178
+
179
+ mkdir -p "$AGENT_DIR/extensions" "$SESSION_DIR" "$JOBS_DIR"
180
+
181
+ cat > "$AGENT_DIR/extensions/oracle.json" <<JSON
182
+ {
183
+ "auth": {
184
+ "chromeCookiePath": "$TEST_ROOT/missing-cookies.sqlite"
185
+ }
186
+ }
187
+ JSON
188
+
189
+ cat > "$FAKE_BROWSER" <<'SH'
190
+ #!/bin/sh
191
+ trap 'exit 0' TERM INT
192
+ while :; do sleep 1; done
193
+ SH
194
+ chmod +x "$FAKE_BROWSER"
195
+
196
+ cleanup() {
197
+ tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
198
+ }
199
+ trap 'cleanup; rm -rf "$TEST_ROOT"' EXIT
200
+ cleanup
201
+
202
+ 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'"
203
+
204
+ tmux new-session -d -s "$SESSION_NAME" "$TMUX_CMD"
205
+ sleep 8
206
+ tmux send-keys -t "$SESSION_NAME":0.0 '/oracle-auth' Enter
207
+ sleep 12
208
+
209
+ tmux capture-pane -p -S -220 -t "$SESSION_NAME":0.0 | tail -n 140
210
+ ```
211
+
212
+ Expected behavior:
213
+
214
+ - the isolated `pi` session loads the local extension successfully
215
+ - `/oracle-auth` returns with an error instead of hanging indefinitely
216
+ - the output should mention the missing ChatGPT session-token cookies or the configured cookie source problem
217
+ - the session should remain usable after the command failure
218
+
219
+ ## Switching to `thinking_light`
220
+
221
+ To run the same smoke test with `thinking_light`, change both prompts from:
222
+
223
+ ```text
224
+ preset "instant"
225
+ ```
226
+
227
+ to:
228
+
229
+ ```text
230
+ preset "thinking_light"
231
+ ```
232
+
233
+ ## Cleanup
234
+
235
+ The snippet already kills the temporary `tmux` sessions on exit.
236
+
237
+ To remove the temporary files after inspection:
238
+
239
+ ```bash
240
+ rm -rf "$TEST_ROOT"
241
+ ```
242
+
243
+ ## Minimum pre-commit evidence
244
+
245
+ Before committing code changes, keep evidence for:
246
+
247
+ - `npm test` passing
248
+ - isolated `pi` session validation using this workflow
249
+ - 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,15 @@
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";
2
7
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
+ import { formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
3
9
  import { loadOracleConfig } from "./config.js";
4
10
  import {
5
11
  cancelOracleJob,
12
+ getJobDir,
6
13
  isOpenOracleJob,
7
14
  isTerminalOracleJob,
8
15
  listJobsForCwd,
@@ -22,30 +29,10 @@ function summarizeJob(jobId: string): string {
22
29
  const job = readJob(jobId);
23
30
  if (!job) return `Oracle job ${jobId} not found.`;
24
31
 
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");
32
+ return formatOracleJobSummary(job, {
33
+ queuePosition: job.status === "queued" ? getQueuePosition(job.id) : undefined,
34
+ artifactsPath: `${getJobDir(job.id)}/artifacts`,
35
+ });
49
36
  }
50
37
 
51
38
  function getLatestJobId(cwd: string): string | undefined {
@@ -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";