pi-oracle 0.4.0 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +25 -8
- package/docs/ORACLE_DESIGN.md +14 -8
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +28 -1
- package/extensions/oracle/lib/commands.ts +14 -5
- package/extensions/oracle/lib/config.ts +51 -2
- package/extensions/oracle/lib/jobs.ts +17 -2
- package/extensions/oracle/lib/poller.ts +25 -2
- package/extensions/oracle/lib/runtime.ts +42 -5
- package/extensions/oracle/lib/tools.ts +615 -247
- 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 +26 -8
- package/extensions/oracle/worker/auth-bootstrap.mjs +49 -4
- package/package.json +1 -1
- package/prompts/oracle.md +16 -10
|
@@ -104,6 +104,7 @@ export declare function appendOracleJobLifecycleEvent<TJob extends OracleLifecyc
|
|
|
104
104
|
event: Omit<OracleJobLifecycleEvent, "status" | "phase"> & { status?: OracleJobStatus; phase?: OracleJobPhase },
|
|
105
105
|
): TJob;
|
|
106
106
|
export declare function getLatestOracleJobLifecycleEvent(job: Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">): OracleJobLifecycleEvent | undefined;
|
|
107
|
+
export declare function getLatestOracleTerminalLifecycleEvent(job: Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">): OracleJobLifecycleEvent | undefined;
|
|
107
108
|
export declare function markOracleJobCreated<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, options?: { at?: string; source?: string; message?: string }): TJob;
|
|
108
109
|
export declare function transitionOracleJobPhase<TJob extends OracleLifecycleTrackedJobLike>(
|
|
109
110
|
job: TJob,
|
|
@@ -111,6 +111,19 @@ export function getLatestOracleJobLifecycleEvent(job) {
|
|
|
111
111
|
return events.length > 0 ? events[events.length - 1] : undefined;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">} job
|
|
116
|
+
* @returns {OracleJobLifecycleEvent | undefined}
|
|
117
|
+
*/
|
|
118
|
+
export function getLatestOracleTerminalLifecycleEvent(job) {
|
|
119
|
+
const events = job.lifecycleEvents || [];
|
|
120
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
121
|
+
const event = events[index];
|
|
122
|
+
if (event?.kind === "phase" && TERMINAL_ORACLE_JOB_STATUSES.includes(event.status)) return event;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
/**
|
|
115
128
|
* @template {OracleLifecycleTrackedJobLike} TJob
|
|
116
129
|
* @param {TJob} job
|
|
@@ -32,6 +32,7 @@ export interface OracleJobSummaryOptions {
|
|
|
32
32
|
queuePosition?: OracleQueuePositionLike;
|
|
33
33
|
artifactsPath?: string;
|
|
34
34
|
responsePreview?: string;
|
|
35
|
+
responseAvailable?: boolean;
|
|
35
36
|
includeLatestEvent?: boolean;
|
|
36
37
|
includeWorkerLogPath?: boolean;
|
|
37
38
|
}
|
|
@@ -53,7 +54,7 @@ export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEven
|
|
|
53
54
|
export declare function formatOracleJobSummary(job: OracleJobSummaryLike, options?: OracleJobSummaryOptions): string;
|
|
54
55
|
export declare function buildOracleWakeupNotificationContent(
|
|
55
56
|
job: OracleJobSummaryLike,
|
|
56
|
-
options?: { responsePath?: string; artifactsPath?: string },
|
|
57
|
+
options?: { responsePath?: string; responseAvailable?: boolean; artifactsPath?: string },
|
|
57
58
|
): string;
|
|
58
59
|
export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
|
|
59
60
|
export declare function buildOracleStatusText(counts: OracleStatusCounts): string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Usage: Imported by commands, tools, poller, and extension startup/status code to keep detached-oracle messaging consistent.
|
|
5
5
|
// Invariants/Assumptions: Job summaries read from durable job state, and lifecycle event trails are bounded and already normalized by shared lifecycle helpers.
|
|
6
6
|
|
|
7
|
-
import { getLatestOracleJobLifecycleEvent } from "./job-lifecycle-helpers.mjs";
|
|
7
|
+
import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent } from "./job-lifecycle-helpers.mjs";
|
|
8
8
|
|
|
9
9
|
/** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryLike} OracleJobSummaryLike */
|
|
10
10
|
/** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryOptions} OracleJobSummaryOptions */
|
|
@@ -52,7 +52,22 @@ function formatAutoPrunedArchiveMessage(autoPrunedPrefixes) {
|
|
|
52
52
|
* @returns {string}
|
|
53
53
|
*/
|
|
54
54
|
export function formatOracleJobSummary(job, options = {}) {
|
|
55
|
-
const
|
|
55
|
+
const latestEventRaw = options.includeLatestEvent === false ? undefined : getLatestOracleJobLifecycleEvent(job);
|
|
56
|
+
const terminalEventRaw = getLatestOracleTerminalLifecycleEvent(job);
|
|
57
|
+
const latestEvent = formatOracleLifecycleEvent(latestEventRaw);
|
|
58
|
+
const terminalEvent = formatOracleLifecycleEvent(terminalEventRaw);
|
|
59
|
+
const sameEvent = Boolean(
|
|
60
|
+
latestEventRaw && terminalEventRaw &&
|
|
61
|
+
latestEventRaw.at === terminalEventRaw.at &&
|
|
62
|
+
latestEventRaw.source === terminalEventRaw.source &&
|
|
63
|
+
latestEventRaw.kind === terminalEventRaw.kind &&
|
|
64
|
+
latestEventRaw.message === terminalEventRaw.message,
|
|
65
|
+
);
|
|
66
|
+
const responseLine = options.responseAvailable === true
|
|
67
|
+
? job.responsePath ? `response: ${job.responsePath}` : undefined
|
|
68
|
+
: job.responsePath ? "response: unavailable yet" : undefined;
|
|
69
|
+
const responseFormatLine = options.responseAvailable === true && job.responseFormat ? `response-format: ${job.responseFormat}` : undefined;
|
|
70
|
+
const latestEventLabel = latestEventRaw?.kind === "wakeup" ? "wakeup-event" : "last-event";
|
|
56
71
|
return [
|
|
57
72
|
`job: ${job.id}`,
|
|
58
73
|
`status: ${job.status}`,
|
|
@@ -67,14 +82,15 @@ export function formatOracleJobSummary(job, options = {}) {
|
|
|
67
82
|
job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
|
|
68
83
|
job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
|
|
69
84
|
job.conversationId ? `conversation: ${job.conversationId}` : undefined,
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
responseLine,
|
|
86
|
+
responseFormatLine,
|
|
72
87
|
options.artifactsPath ? `artifacts: ${options.artifactsPath}` : undefined,
|
|
73
88
|
typeof job.artifactFailureCount === "number" ? `artifact-failures: ${job.artifactFailureCount}` : undefined,
|
|
74
89
|
options.includeWorkerLogPath === false ? undefined : job.workerLogPath ? `worker-log: ${job.workerLogPath}` : undefined,
|
|
75
90
|
job.lastCleanupAt ? `last-cleanup: ${job.lastCleanupAt}` : undefined,
|
|
76
91
|
job.cleanupWarnings?.length ? `cleanup-warnings: ${job.cleanupWarnings.join(" | ")}` : undefined,
|
|
77
|
-
|
|
92
|
+
terminalEvent ? `terminal-event: ${terminalEvent}` : undefined,
|
|
93
|
+
latestEvent && !sameEvent ? `${latestEventLabel}: ${latestEvent}` : undefined,
|
|
78
94
|
job.error ? `error: ${job.error}` : undefined,
|
|
79
95
|
options.responsePreview ? "" : undefined,
|
|
80
96
|
options.responsePreview,
|
|
@@ -85,16 +101,18 @@ export function formatOracleJobSummary(job, options = {}) {
|
|
|
85
101
|
|
|
86
102
|
/**
|
|
87
103
|
* @param {OracleJobSummaryLike} job
|
|
88
|
-
* @param {{ responsePath?: string; artifactsPath?: string }} [options]
|
|
104
|
+
* @param {{ responsePath?: string; responseAvailable?: boolean; artifactsPath?: string }} [options]
|
|
89
105
|
* @returns {string}
|
|
90
106
|
*/
|
|
91
107
|
export function buildOracleWakeupNotificationContent(job, options = {}) {
|
|
92
|
-
const
|
|
108
|
+
const responseLine = options.responseAvailable === false
|
|
109
|
+
? "Response file: unavailable yet"
|
|
110
|
+
: `Response file: ${options.responsePath ?? job.responsePath ?? `response unavailable for ${job.id}`}`;
|
|
93
111
|
const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
|
|
94
112
|
return [
|
|
95
113
|
`Oracle job ${job.id} is ${job.status}.`,
|
|
96
114
|
`Use oracle_read with jobId ${job.id} to open the response and settle wake-up retries.`,
|
|
97
|
-
|
|
115
|
+
responseLine,
|
|
98
116
|
`Artifacts: ${artifactsPath}`,
|
|
99
117
|
formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
|
|
100
118
|
job.error ? `Error: ${job.error}` : "After oracle_read, continue from the oracle output.",
|
|
@@ -20,7 +20,23 @@ if (!rawConfig) {
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const parsedConfigPayload = JSON.parse(rawConfig);
|
|
24
|
+
const config =
|
|
25
|
+
parsedConfigPayload &&
|
|
26
|
+
typeof parsedConfigPayload === "object" &&
|
|
27
|
+
!Array.isArray(parsedConfigPayload) &&
|
|
28
|
+
Object.hasOwn(parsedConfigPayload, "config")
|
|
29
|
+
? parsedConfigPayload.config
|
|
30
|
+
: parsedConfigPayload;
|
|
31
|
+
const configLoad =
|
|
32
|
+
parsedConfigPayload &&
|
|
33
|
+
typeof parsedConfigPayload === "object" &&
|
|
34
|
+
!Array.isArray(parsedConfigPayload) &&
|
|
35
|
+
Object.hasOwn(parsedConfigPayload, "configLoad") &&
|
|
36
|
+
parsedConfigPayload.configLoad &&
|
|
37
|
+
typeof parsedConfigPayload.configLoad === "object"
|
|
38
|
+
? parsedConfigPayload.configLoad
|
|
39
|
+
: undefined;
|
|
24
40
|
const CHATGPT_LABELS = {
|
|
25
41
|
composer: "Chat with ChatGPT",
|
|
26
42
|
addFiles: "Add files and more",
|
|
@@ -65,6 +81,34 @@ function authSessionName() {
|
|
|
65
81
|
return `${config.browser.sessionPrefix}-auth`;
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
function effectiveAuthConfigPath() {
|
|
85
|
+
return typeof configLoad?.effectiveAuthConfigPath === "string" && configLoad.effectiveAuthConfigPath
|
|
86
|
+
? configLoad.effectiveAuthConfigPath
|
|
87
|
+
: join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "extensions", "oracle.json");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authConfigRemediation() {
|
|
91
|
+
if (typeof configLoad?.remediation === "string" && configLoad.remediation) return configLoad.remediation;
|
|
92
|
+
if (typeof configLoad?.projectConfigPath === "string" && configLoad.projectConfigPath && configLoad.projectConfigExists) {
|
|
93
|
+
return (
|
|
94
|
+
`Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}. ` +
|
|
95
|
+
`Project overrides are also read from ${configLoad.projectConfigPath}, but auth.* is loaded from ${effectiveAuthConfigPath()}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return `Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function authConfigSummary() {
|
|
102
|
+
if (typeof configLoad?.summary === "string" && configLoad.summary) return configLoad.summary;
|
|
103
|
+
const agentDirSuffix = typeof configLoad?.agentDir === "string" && configLoad.agentDir ? ` (agent dir: ${configLoad.agentDir})` : "";
|
|
104
|
+
const createSuffix = configLoad?.agentConfigExists === false ? " [create this file to override auth.*]" : "";
|
|
105
|
+
const lines = [`Effective oracle auth config: ${effectiveAuthConfigPath()}${agentDirSuffix}${createSuffix}`];
|
|
106
|
+
if (typeof configLoad?.projectConfigPath === "string" && configLoad.projectConfigPath && configLoad.projectConfigExists) {
|
|
107
|
+
lines.push(`Project oracle config also loaded: ${configLoad.projectConfigPath} (auth.* still comes from ${effectiveAuthConfigPath()}).`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
function sleep(ms) {
|
|
69
113
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
114
|
}
|
|
@@ -457,7 +501,7 @@ async function readSourceCookies() {
|
|
|
457
501
|
|
|
458
502
|
if (!hasSessionToken) {
|
|
459
503
|
throw new Error(
|
|
460
|
-
`No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that Chrome profile
|
|
504
|
+
`No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that Chrome profile. ${authConfigRemediation()}`,
|
|
461
505
|
);
|
|
462
506
|
}
|
|
463
507
|
|
|
@@ -731,7 +775,7 @@ async function waitForImportedAuthReady() {
|
|
|
731
775
|
}
|
|
732
776
|
if (classification.state === "login_required") {
|
|
733
777
|
await captureDiagnostics("login-required");
|
|
734
|
-
throw new Error(classification.message);
|
|
778
|
+
throw new Error(`${classification.message} ${authConfigRemediation()}`);
|
|
735
779
|
}
|
|
736
780
|
await sleep(config.auth.pollMs);
|
|
737
781
|
}
|
|
@@ -751,6 +795,7 @@ async function run() {
|
|
|
751
795
|
await log(
|
|
752
796
|
`Config summary: session=${authSessionName()} seedProfileDir=${profilePlan.targetDir} stagingProfileDir=${profilePlan.stagingDir} executable=${config.browser.executablePath || "(default)"} source=${cookieSourceLabel()}`,
|
|
753
797
|
);
|
|
798
|
+
await log(authConfigSummary());
|
|
754
799
|
const cookies = await readSourceCookies();
|
|
755
800
|
await prepareStagedProfile(profilePlan);
|
|
756
801
|
await launchTargetBrowser();
|
|
@@ -783,7 +828,7 @@ async function run() {
|
|
|
783
828
|
|
|
784
829
|
run().catch((error) => {
|
|
785
830
|
process.stderr.write(
|
|
786
|
-
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}\nIf needed, ensure the configured real Chrome profile is already logged into ChatGPT and grant macOS Keychain access when prompted.`,
|
|
831
|
+
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}\n${authConfigSummary()}\nIf needed, ensure the configured real Chrome profile is already logged into ChatGPT and grant macOS Keychain access when prompted.`,
|
|
787
832
|
);
|
|
788
833
|
process.exit(1);
|
|
789
834
|
});
|
package/package.json
CHANGED
package/prompts/oracle.md
CHANGED
|
@@ -6,29 +6,35 @@ You are preparing an /oracle job.
|
|
|
6
6
|
Do not answer the user's request directly yet.
|
|
7
7
|
|
|
8
8
|
Required workflow:
|
|
9
|
-
1.
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
13
|
-
5.
|
|
14
|
-
6.
|
|
9
|
+
1. Call `oracle_preflight` immediately.
|
|
10
|
+
2. If `oracle_preflight` reports `ready: false`, stop immediately and report the blocking issue plus the suggested next step. Do not read files, search the codebase, or prepare archive inputs first.
|
|
11
|
+
3. Understand the request and decide whether it is explicitly narrow or genuinely broad.
|
|
12
|
+
4. Gather only the smallest repo context needed to choose archive inputs and write a strong oracle prompt.
|
|
13
|
+
5. If the user scope is explicit and narrow, prefer a minimal targeted read/search set and dispatch as soon as you have enough context. Do not broaden into repo-wide exploration unless the narrow pass proves insufficient.
|
|
14
|
+
6. If the request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
|
|
15
|
+
7. Choose archive inputs for the oracle job.
|
|
16
|
+
8. Craft a concise but complete oracle prompt for ChatGPT web.
|
|
17
|
+
9. Call `oracle_submit` with the prompt and exact archive inputs.
|
|
18
|
+
10. Stop immediately after dispatching the oracle job.
|
|
15
19
|
|
|
16
20
|
Oracle model (`oracle_submit`):
|
|
17
21
|
- To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the canonical preset registry.
|
|
18
22
|
- Matching human-readable preset labels and common hyphen/space variants are also accepted and normalized automatically, but prefer canonical ids when readily available.
|
|
19
23
|
- **Or** omit **`preset`** entirely to use the configured default model (from oracle config).
|
|
20
24
|
- **`preset`** is the only model-selection parameter on `oracle_submit`. Do not pass `modelFamily`, `effort`, or `autoSwitchToThinking`.
|
|
21
|
-
- If unsure
|
|
25
|
+
- If unsure, omit **`preset`** and use the configured default. Ask the user about model choice only when they explicitly want model control or the choice would materially change the result.
|
|
22
26
|
|
|
23
27
|
Rules:
|
|
28
|
+
- Use `oracle_preflight` before any expensive `/oracle` preparation so missing persisted-session or local auth/config blockers fail fast.
|
|
24
29
|
- Always include an archive. Do not submit without context files.
|
|
25
|
-
- By default, include the whole repository by passing
|
|
30
|
+
- By default, include the whole repository by passing `.` when the request is genuinely broad, repo-wide, or unclear after a quick narrow pass. Default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like `.env` files, key material, credential dotfiles, local database files, and nested `secrets/` directories anywhere in the repo.
|
|
26
31
|
- Only limit file selection if the user explicitly requests it, if the task is clearly scoped to a smaller area, or if privacy/sensitivity requires it.
|
|
27
|
-
- For very targeted asks like reviewing one function
|
|
32
|
+
- For very targeted asks like reviewing one function, one file, one stack trace, or one narrowly scoped question, start with the smallest obviously sufficient archive and expand only if that first pass proves insufficient.
|
|
33
|
+
- Do not keep exploring once you already have enough context to submit well.
|
|
28
34
|
- If the request depends on git state or pending changes (for example code review, ship readiness, or release approval), create a tracked diff bundle file inside the repo (for example under `.pi/`) containing `git status` plus `git diff` output, include that file in the archive, and tell the oracle to use it because the `.git` directory is not included in oracle exports.
|
|
29
35
|
- When `files=["."]` and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like `build/`, `dist/`, `out/`, `coverage/`, and `tmp/` outside obvious source roots like `src/` and `lib/` until the archive fits or no candidate remains. Successful submissions report what was pruned.
|
|
30
36
|
- If a submitted oracle job later fails because upload is rejected, retry with a smaller archive in this order: (1) remove the largest obviously irrelevant/generated content, (2) if still too large, include modified files plus adjacent files plus directly relevant subtrees, (3) if still too large, explain the cut or ask the user.
|
|
31
|
-
- Prefer the configured default (omit **`preset`**) unless the task clearly needs a different model; then choose a canonical **`preset`** id.
|
|
37
|
+
- Prefer the configured default (omit **`preset`**) unless the task clearly needs a different model or the user explicitly asked for one; then choose a canonical **`preset`** id.
|
|
32
38
|
- If `oracle_submit` itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error. Do not retry automatically.
|
|
33
39
|
- If `oracle_submit` returns a queued job instead of an immediately dispatched one, treat that as success and end your turn exactly the same way.
|
|
34
40
|
- After oracle_submit returns, end your turn. Do not keep working while the oracle runs.
|