pi-oracle 0.7.12 → 0.7.14
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 +29 -0
- package/README.md +16 -5
- package/docs/ORACLE_DESIGN.md +9 -5
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +22 -3
- package/docs/platform-smoke.md +4 -3
- package/extensions/oracle/lib/archive.ts +2 -0
- package/extensions/oracle/lib/config.ts +2 -2
- package/extensions/oracle/lib/jobs.ts +47 -2
- package/extensions/oracle/lib/runtime.ts +3 -2
- package/extensions/oracle/lib/tools.ts +3 -3
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +23 -3
- package/extensions/oracle/worker/run-job.mjs +72 -20
- package/package.json +10 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/oracle-chatgpt-preset-proof.mjs +352 -0
- package/scripts/platform-smoke/invariants.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.14 - 2026-06-22
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- updated the local pi development and validation baseline to `@earendil-works/*` `0.79.10`
|
|
9
|
+
- refreshed oracle docs and sanity contracts for pi `0.79.10`, and removed the obsolete fleet-tested marker
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- used pi's exported `CONFIG_DIR_NAME` for project config and workspace-root detection instead of hardcoding `.pi`
|
|
13
|
+
- clarified `oracle_preflight` path labels so isolated-session probes distinguish the current persisted session from the provider auth seed profile
|
|
14
|
+
- fixed ChatGPT response completion detection for the current DOM, where assistant text uses `data-message-author-role="assistant"` without legacy `.message-bubble` nodes
|
|
15
|
+
|
|
16
|
+
### Compatibility
|
|
17
|
+
- reviewed the pi `0.79.10` changelog, extension lifecycle docs/types, compaction event docs, project-trust docs, and package/update docs; no oracle compaction hook changes were required
|
|
18
|
+
|
|
19
|
+
### Validation
|
|
20
|
+
- ran `npm run verify:oracle`, `npm run smoke:real:packed`, source-mode isolated pi model-agent smoke with the `instant` preset, and `npm run smoke:platform:all`
|
|
21
|
+
|
|
22
|
+
## 0.7.13 - 2026-06-15
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- added a release-blocking ChatGPT preset proof gate (`npm run release:proof:chatgpt-presets`) so publishing requires fresh loaded-extension evidence for every canonical ChatGPT preset
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- fixed compact ChatGPT Intelligence menu handling so selected thinking tiers that close back to `Medium`, `High`, or `Extra High` composer pills are accepted only after an intentional matching menu click instead of falling through to the removed legacy effort dropdown
|
|
29
|
+
- fixed `instant_auto_switch` under the compact ChatGPT UI, where the legacy auto-switch control is absent after selecting the compact `Instant` tier
|
|
30
|
+
- made ChatGPT model-configuration opening tolerate slower compact-UI hydration before reporting UI drift
|
|
31
|
+
- stabilized archive creation when the compression subprocess exits before tar, so the worker terminates upstream tar immediately instead of waiting for the archive timeout
|
|
32
|
+
- surfaced provider rate-limit/outage modals explicitly during ChatGPT model setup, upload, send, and response waits instead of reporting generic UI drift
|
|
33
|
+
|
|
5
34
|
## 0.7.12 - 2026-06-15
|
|
6
35
|
|
|
7
36
|
### Changed
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
|
|
4
4
|
|
|
5
|
-
> Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.
|
|
5
|
+
> Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.10`. Pi `0.79.10+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
|
|
6
6
|
|
|
7
7
|
## What a successful run looks like
|
|
8
8
|
|
|
@@ -77,7 +77,7 @@ You need:
|
|
|
77
77
|
|
|
78
78
|
- macOS, Linux, or Windows native
|
|
79
79
|
- Node.js 22 or newer
|
|
80
|
-
- Suggested tested floor: `pi` 0.79.
|
|
80
|
+
- Suggested tested floor: `pi` 0.79.10 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
|
|
81
81
|
- Google Chrome/Chromium or another Chromium-family browser
|
|
82
82
|
- ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
|
|
83
83
|
- `agent-browser` and `tar` available on the machine; `zstd` is also required when submitting ChatGPT `.tar.zst` archives
|
|
@@ -184,7 +184,7 @@ Agent-facing tools:
|
|
|
184
184
|
|
|
185
185
|
Most users can start with defaults. Set an agent-level config only when you need a non-default provider, mode, preset, or browser profile.
|
|
186
186
|
|
|
187
|
-
Pi 0.79
|
|
187
|
+
Pi 0.79+ gates project-local inputs behind project trust. `pi-oracle` preserves its historical risk-on extension behavior for existing users: project-local `.pi/extensions/oracle.json` safe overrides still load by default for compatibility. They are ignored when you explicitly opt out of project-local inputs with `--no-approve` or save a “do not trust” decision for the project. Privileged browser/auth settings still come only from the agent-level config.
|
|
188
188
|
|
|
189
189
|
`~/.pi/agent/extensions/oracle.json`
|
|
190
190
|
|
|
@@ -382,7 +382,7 @@ npm test
|
|
|
382
382
|
npm run verify:oracle
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
-
`npm publish` is guarded by `prepublishOnly`, which runs `npm run release:check`. That release gate requires doctor-first macOS, Ubuntu, and Windows native Crabbox evidence. The required Crabbox runtime suite uses packed-install proof, not source-tree `pi -e` loading.
|
|
385
|
+
`npm publish` is guarded by `prepublishOnly`, which runs `npm run release:check`. That release gate now blocks unless fresh live ChatGPT preset proof exists for every canonical preset, then requires doctor-first macOS, Ubuntu, and Windows native Crabbox evidence. The required Crabbox runtime suite uses packed-install proof, not source-tree `pi -e` loading.
|
|
386
386
|
|
|
387
387
|
Use the narrowest validation workflow that proves the change:
|
|
388
388
|
|
|
@@ -391,6 +391,7 @@ Use the narrowest validation workflow that proves the change:
|
|
|
391
391
|
| Everyday local iteration | `npm run verify:oracle` |
|
|
392
392
|
| Platform-sensitive changes | `npm run smoke:platform:doctor`, then a focused `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` |
|
|
393
393
|
| Platform matrix proof | `npm run smoke:platform:all` |
|
|
394
|
+
| ChatGPT preset release proof | `npm run release:proof:chatgpt-presets` |
|
|
394
395
|
| Publish/release gate | `npm run release:check` |
|
|
395
396
|
|
|
396
397
|
For macOS, Ubuntu, and Windows native package/build plus packed runtime validation, use [`docs/platform-smoke.md`](docs/platform-smoke.md). The full release gate is:
|
|
@@ -399,9 +400,19 @@ For macOS, Ubuntu, and Windows native package/build plus packed runtime validati
|
|
|
399
400
|
npm run release:check
|
|
400
401
|
```
|
|
401
402
|
|
|
403
|
+
Before a release, run live jobs through the loaded extension for every ChatGPT preset in `ORACLE_SUBMIT_PRESETS`. Each prompt must make the saved response contain exact markers `PRESET <preset> OK` and `PACKAGE pi-oracle`. After every job has completed, save the job ids/job directories in `.artifacts/chatgpt-preset-proof/latest.json`; `validatedAt` must be later than the completed jobs. Start from the checked, intentionally non-valid template:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
mkdir -p .artifacts/chatgpt-preset-proof
|
|
407
|
+
node scripts/oracle-chatgpt-preset-proof.mjs template > .artifacts/chatgpt-preset-proof/latest.json
|
|
408
|
+
npm run release:proof:chatgpt-presets
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The proof checker is intentionally part of `release:check`; it fails if the proof is missing, stale, tied to a different package version/git head, references jobs that completed before the current commit, or lacks actual persisted ChatGPT `.tar.zst` job state and response text for any canonical preset.
|
|
412
|
+
|
|
402
413
|
The real runtime suite defaults to deterministic installed-tool execution so platform proof stays bounded. Provider/model defaults remain `zai/glm-5.2` for doctor/config and for optional model-agent debugging; override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL` when needed. For inner-loop source loading only, use `npm run smoke:real:source`; it is not release proof. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when debugging the slower model-agent path. The optional second real-agent negative symlink check is opt-in via `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1`; `npm run sanity:oracle` covers archive/symlink rejection by default without adding another model-agent turn to the platform release gate.
|
|
403
414
|
|
|
404
|
-
For manual end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md).
|
|
415
|
+
For manual end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md). Ordinary pre-commit smoke runs can still use `instant` or `thinking_light`, but release proof must cover every canonical ChatGPT preset through the loaded extension.
|
|
405
416
|
|
|
406
417
|
## Project map
|
|
407
418
|
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -7,7 +7,7 @@ Companion doc:
|
|
|
7
7
|
- `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
|
|
8
8
|
|
|
9
9
|
Compatibility target:
|
|
10
|
-
- `pi` 0.79.
|
|
10
|
+
- `pi` 0.79.10+ is the suggested tested floor for current project-trust-aware package/runtime validation
|
|
11
11
|
- package metadata keeps pi runtime packages as optional wildcard peers, so this suggested floor is not enforced as a hard npm install requirement
|
|
12
12
|
- current extension lifecycle only; no backward-compatibility shims for removed `session_switch` / `session_fork` events
|
|
13
13
|
|
|
@@ -234,7 +234,7 @@ Merged config locations:
|
|
|
234
234
|
- global: `~/.pi/agent/extensions/oracle.json`
|
|
235
235
|
- project: `.pi/extensions/oracle.json`
|
|
236
236
|
|
|
237
|
-
Project config remains restricted to safe overrides only. On Pi 0.79
|
|
237
|
+
Project config remains restricted to safe overrides only. On Pi 0.79+, pi itself gates project-local inputs behind project trust, but `pi-oracle` keeps its historical risk-on extension behavior for this package-specific safe override file: `.pi/extensions/oracle.json` loads by default for compatibility, and is ignored when Pi reports the project is untrusted, including `--no-approve` or saved “do not trust” decisions. This preserves the existing extension experience while still honoring explicit opt-out/distrust decisions. Browser/auth settings remain global-only because they control local privileged browser state.
|
|
238
238
|
|
|
239
239
|
### Current config shape
|
|
240
240
|
|
|
@@ -608,7 +608,7 @@ Live-validated after the concurrency redesign:
|
|
|
608
608
|
|
|
609
609
|
Still to verify live after this pivot:
|
|
610
610
|
|
|
611
|
-
-
|
|
611
|
+
- full ChatGPT preset release matrix evidence must be refreshed before any release; `npm run release:proof:chatgpt-presets` blocks release without one completed loaded-extension ChatGPT job for every canonical preset
|
|
612
612
|
- optional richer terminal semantics for partial artifact failure (`complete_with_artifact_errors`) in more live scenarios
|
|
613
613
|
|
|
614
614
|
## Production readiness criteria
|
|
@@ -629,7 +629,7 @@ This architecture is now live-validated for the core release path:
|
|
|
629
629
|
### Current readiness summary
|
|
630
630
|
|
|
631
631
|
Current release blockers for the validated scope:
|
|
632
|
-
-
|
|
632
|
+
- release is blocked until fresh loaded-extension ChatGPT preset proof passes `npm run release:proof:chatgpt-presets` for every canonical `ORACLE_SUBMIT_PRESETS` id
|
|
633
633
|
|
|
634
634
|
Remaining non-blocking hardening work:
|
|
635
635
|
- broaden live proof of the new lifecycle/state-machine model across more degraded paths
|
|
@@ -639,6 +639,10 @@ Remaining non-blocking hardening work:
|
|
|
639
639
|
- keep hardening model-selection verification against future ChatGPT UI variation
|
|
640
640
|
|
|
641
641
|
Recent proof points:
|
|
642
|
+
- Pi 0.79.10 local gate: `npm run verify:oracle` passed on 2026-06-22 after the 0.79.10 baseline refresh and `CONFIG_DIR_NAME` cleanup
|
|
643
|
+
- Pi 0.79.10 isolated extension smokes: `.artifacts/real-smoke/run-1782137209549-0xe67z` passed packed-install proof, and `.artifacts/real-smoke/run-1782137217821-95a1po` passed source model-agent proof
|
|
644
|
+
- Pi 0.79.10 platform artifacts: `.artifacts/platform-smoke/run-1782137574391-7lay68` (macOS platform-build), `.artifacts/platform-smoke/run-1782137619352-gku7jz` (macOS real-extension), `.artifacts/platform-smoke/run-1782137587082-d7kg4p` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1782137619176-lgxezy` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1782137625964-66z0oc` (Windows native platform-build), `.artifacts/platform-smoke/run-1782137752969-pbmdj1` (Windows native real-extension)
|
|
645
|
+
- Pi 0.79.10 isolated agent feedback: `.artifacts/isolated-agent-feedback/run-1782137385` confirmed local extension loading and useful `oracle_preflight` output after the path-label polish
|
|
642
646
|
- Pi 0.79.1 release gate: `npm run release:check` passed on 2026-06-11 after the project-trust, prompt-history, ChatGPT selector, and send-acceptance updates, including `verify:oracle` plus Crabbox macOS, Ubuntu, and Windows native `platform-build` and `real-extension` suites
|
|
643
647
|
- Pi 0.79.1 platform artifacts: `.artifacts/platform-smoke/run-1781196218405-311wzs` (macOS platform-build), `.artifacts/platform-smoke/run-1781196261807-eb0391` (macOS real-extension), `.artifacts/platform-smoke/run-1781196230636-ze1hai` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1781196265638-kxiwh9` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1781196255488-ucuf35` (Windows native platform-build), `.artifacts/platform-smoke/run-1781196369098-4qlzjs` (Windows native real-extension)
|
|
644
648
|
- Pi 0.79.1 live source-extension send-acceptance smoke: new-chat job `4b98776f-d422-4bfb-8a6a-7aef73c31bf6` reached `https://chatgpt.com/c/6a2ac99d-fc5c-83e8-88d7-5e1e8f427499` and completed; same-thread follow-up job `abb4f590-96a1-4aab-b91a-c0a7cc15a162` completed on the unchanged conversation URL after send-acceptance evidence
|
|
@@ -653,4 +657,4 @@ Recent proof points:
|
|
|
653
657
|
- repo-owned sanity harness: `npm run sanity:oracle`
|
|
654
658
|
- real installed-extension smoke source of truth: `scripts/oracle-real-smoke.mjs`; required release proof runs packed-install mode (`npm run smoke:real:packed`) and executes installed-package `oracle_submit` deterministically, with optional slower model-agent debugging via `PI_ORACLE_REAL_TEST_MODEL_AGENT=1`; source mode (`npm run smoke:real:source`) is inner-loop/debug only
|
|
655
659
|
- macOS, Ubuntu, and Windows native package/build/runtime smoke source of truth: `docs/platform-smoke.md`; use `npm run verify:oracle` for everyday local iteration, `npm run smoke:platform:doctor` plus a focused target/suite run for platform-sensitive changes, `npm run smoke:platform:all` for doctor-first platform matrix evidence, and `npm run release:check` for the full local-plus-platform release gate
|
|
656
|
-
- release gate: `npm run release:check`, also used by `prepublishOnly`, combines static verification and all required Crabbox platform smokes
|
|
660
|
+
- release gate: `npm run release:check`, also used by `prepublishOnly`, combines static verification, fresh loaded-extension ChatGPT preset proof via `npm run release:proof:chatgpt-presets`, and all required Crabbox platform smokes
|
|
@@ -27,7 +27,7 @@ The extension is loaded from the local checkout with:
|
|
|
27
27
|
pi --approve --no-extensions -e "$REPO/extensions/oracle/index.ts"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79
|
|
30
|
+
That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79+: the test fixture is this trusted checkout, and non-interactive/scripted validation must not block on the project-trust prompt.
|
|
31
31
|
|
|
32
32
|
The local extension now intercepts TUI `/oracle` and `/oracle-followup` before prompt-template expansion, re-injects the compact slash request as the visible user message for prompt-history/up-arrow recall, and reads the in-repo prompt files as hidden dispatch instructions, so do not pass `--prompt-template` for normal local-extension validation. In print/json/rpc modes, the extension contributes the prompt templates itself.
|
|
33
33
|
|
|
@@ -35,15 +35,34 @@ Do not add `https://github.com/fitchmultz/pi-oracle` to this repository's `.pi/s
|
|
|
35
35
|
|
|
36
36
|
`oracle_submit` now preflights missing, unreadable, or unverified auth seed profiles before it creates an archive or persists a job. For archive-inspection smoke tests that intentionally run without real auth, use `oracle_preflight` for the blocker path or create a test seed only in a purpose-built fixture that includes the `.oracle-seed-generation` marker.
|
|
37
37
|
|
|
38
|
-
## Preset
|
|
38
|
+
## Preset requirements
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
For ordinary pre-commit isolated smoke tests, use either:
|
|
41
41
|
|
|
42
42
|
- `instant`
|
|
43
43
|
- `thinking_light`
|
|
44
44
|
|
|
45
45
|
The examples below use `instant` because it is the fastest smoke-test preset.
|
|
46
46
|
|
|
47
|
+
For any release, and for any change that touches ChatGPT model selection, run live loaded-extension jobs for every canonical ChatGPT preset from `ORACLE_SUBMIT_PRESETS`:
|
|
48
|
+
|
|
49
|
+
- `pro_standard`
|
|
50
|
+
- `pro_extended`
|
|
51
|
+
- `thinking_light`
|
|
52
|
+
- `thinking_standard`
|
|
53
|
+
- `thinking_extended`
|
|
54
|
+
- `thinking_heavy`
|
|
55
|
+
- `instant`
|
|
56
|
+
- `instant_auto_switch`
|
|
57
|
+
|
|
58
|
+
Use prompts that make each saved response contain exact markers `PRESET <preset> OK` and `PACKAGE pi-oracle`. Save the completed job ids/job directories in `.artifacts/chatgpt-preset-proof/latest.json` only after every job completes; `validatedAt` must be later than those completed jobs. The checker reads the actual persisted `job.json`, worker log, and response files. Then run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm run release:proof:chatgpt-presets
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`npm run release:check` runs that proof gate before release. This is intentional: publishing is blocked until every ChatGPT preset has fresh loaded-extension evidence.
|
|
65
|
+
|
|
47
66
|
## Prerequisites
|
|
48
67
|
|
|
49
68
|
- `pi` installed locally
|
package/docs/platform-smoke.md
CHANGED
|
@@ -49,7 +49,8 @@ Use the narrowest workflow that proves the change. Do not run the full platform
|
|
|
49
49
|
| Everyday local iteration | `npm run verify:oracle` | Syntax, bundle, platform-smoke invariants, type checks, oracle sanity, and package dry-run pass locally. |
|
|
50
50
|
| Platform-sensitive change | `npm run smoke:platform:doctor`, then `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` | Target setup is ready and the affected platform/suite works without paying for unrelated targets. |
|
|
51
51
|
| Platform matrix proof | `npm run smoke:platform:all` | Doctor-first packed-install proof passes on every required target and suite. |
|
|
52
|
-
|
|
|
52
|
+
| ChatGPT preset release proof | `npm run release:proof:chatgpt-presets` | Fresh loaded-extension proof exists for every canonical ChatGPT preset. |
|
|
53
|
+
| Publish/release gate | `npm run release:check` | Local verification (`verify:oracle`) passes, fresh ChatGPT preset proof exists, then the doctor-first platform matrix passes. |
|
|
53
54
|
|
|
54
55
|
Platform-sensitive changes include archive behavior, process cleanup, runtime/browser profile handling, package metadata, Crabbox harness code, or anything that may differ across macOS/Linux/Windows.
|
|
55
56
|
|
|
@@ -77,7 +78,7 @@ Full release gate:
|
|
|
77
78
|
npm run release:check
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
`release:check` runs `verify:oracle`
|
|
81
|
+
`release:check` runs `verify:oracle`, then `release:proof:chatgpt-presets`, then `smoke:platform:all`, matching the release order: cheap harness checks, fresh live ChatGPT preset proof, doctor, full matrix, then artifact review. `prepublishOnly` runs `npm run release:check`.
|
|
81
82
|
|
|
82
83
|
## What `platform-build` proves
|
|
83
84
|
|
|
@@ -90,7 +91,7 @@ On each required target, `platform-build`:
|
|
|
90
91
|
5. runs `npm pack`;
|
|
91
92
|
6. creates a fresh target-local pi project;
|
|
92
93
|
7. runs `npm install --no-save <packed tarball>`;
|
|
93
|
-
8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79
|
|
94
|
+
8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79+ project-trust gating intentionally trusts the temporary fixture;
|
|
94
95
|
9. runs `pi list --approve`;
|
|
95
96
|
10. asserts the installed package came from `node_modules/pi-oracle` and did not use `pi -e` / source-extension shortcuts.
|
|
96
97
|
|
|
@@ -578,11 +578,13 @@ async function writeNonWindowsTarArchiveFile(
|
|
|
578
578
|
(code) => {
|
|
579
579
|
targetCode = code;
|
|
580
580
|
targetDone = true;
|
|
581
|
+
if (code !== 0 && tarCode === undefined) terminateChildren();
|
|
581
582
|
finish();
|
|
582
583
|
},
|
|
583
584
|
(error) => {
|
|
584
585
|
targetError = error instanceof Error ? error : new Error(String(error));
|
|
585
586
|
targetDone = true;
|
|
587
|
+
if (tarCode === undefined) terminateChildren();
|
|
586
588
|
finish();
|
|
587
589
|
},
|
|
588
590
|
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { CONFIG_DIR_NAME, getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { isAbsolute, join, normalize } from "node:path";
|
|
11
11
|
import {
|
|
12
12
|
assertNotKnownBrowserUserDataPath,
|
|
@@ -377,7 +377,7 @@ export function getOracleConfigLoadDetails(cwd: string, options?: OracleConfigLo
|
|
|
377
377
|
const agentDir = getAgentDir();
|
|
378
378
|
const projectRoot = getProjectId(cwd);
|
|
379
379
|
const agentConfigPath = join(agentDir, "extensions", "oracle.json");
|
|
380
|
-
const projectConfigPath = join(projectRoot,
|
|
380
|
+
const projectConfigPath = join(projectRoot, CONFIG_DIR_NAME, "extensions", "oracle.json");
|
|
381
381
|
const projectConfigExists = existsSync(projectConfigPath);
|
|
382
382
|
const projectConfigTrusted = isProjectConfigTrusted(projectRoot, agentDir, projectConfigExists, options);
|
|
383
383
|
const projectConfigLoaded = projectConfigExists && projectConfigTrusted;
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// Usage: Imported by oracle commands, tools, queue logic, poller flows, and runtime cleanup/reconciliation paths.
|
|
5
5
|
// Invariants/Assumptions: Job mutations happen under per-job locks, worker identity checks defend against PID reuse, and persisted jobs remain the source of truth.
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
7
8
|
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
8
9
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
9
10
|
import { isAbsolute, join, relative as relativePath, resolve, sep } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
10
12
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
13
|
import {
|
|
12
14
|
ACTIVE_ORACLE_JOB_STATUSES,
|
|
@@ -117,6 +119,14 @@ export interface OracleArtifactRecord {
|
|
|
117
119
|
matchesUploadedArchive?: boolean;
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export interface OracleExtensionProvenance {
|
|
123
|
+
schemaVersion: 1;
|
|
124
|
+
packageName: string;
|
|
125
|
+
packageVersion: string;
|
|
126
|
+
sourcePath: string;
|
|
127
|
+
gitHead?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
export interface OracleJob {
|
|
121
131
|
id: string;
|
|
122
132
|
status: OracleJobStatus;
|
|
@@ -135,6 +145,7 @@ export interface OracleJob {
|
|
|
135
145
|
originSessionFile?: string;
|
|
136
146
|
requestSource: "command" | "tool";
|
|
137
147
|
selection: OracleResolvedSelection;
|
|
148
|
+
extensionProvenance?: OracleExtensionProvenance;
|
|
138
149
|
followUpToJobId?: string;
|
|
139
150
|
chatUrl?: string;
|
|
140
151
|
conversationId?: string;
|
|
@@ -452,8 +463,8 @@ export async function cleanupJobResources(
|
|
|
452
463
|
|
|
453
464
|
function getCleanupRetentionMs(job: OracleJob): { complete: number; failed: number } {
|
|
454
465
|
return {
|
|
455
|
-
complete: job.config
|
|
456
|
-
failed: job.config
|
|
466
|
+
complete: job.config?.cleanup?.completeJobRetentionMs ?? ORACLE_COMPLETE_JOB_RETENTION_MS,
|
|
467
|
+
failed: job.config?.cleanup?.failedJobRetentionMs ?? ORACLE_FAILED_JOB_RETENTION_MS,
|
|
457
468
|
};
|
|
458
469
|
}
|
|
459
470
|
|
|
@@ -899,6 +910,39 @@ export async function cancelOracleJob(id: string, reason = "Cancelled by user"):
|
|
|
899
910
|
});
|
|
900
911
|
}
|
|
901
912
|
|
|
913
|
+
function readExtensionProvenance(cwd: string): OracleExtensionProvenance {
|
|
914
|
+
const sourcePath = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
915
|
+
let packageName = "pi-oracle";
|
|
916
|
+
let packageVersion = "unknown";
|
|
917
|
+
try {
|
|
918
|
+
const packageJson = JSON.parse(readFileSync(join(sourcePath, "package.json"), "utf8")) as { name?: string; version?: string };
|
|
919
|
+
packageName = packageJson.name || packageName;
|
|
920
|
+
packageVersion = packageJson.version || packageVersion;
|
|
921
|
+
} catch {
|
|
922
|
+
// Keep provenance present even when package metadata is unavailable in an
|
|
923
|
+
// unusual loader; release proof rejects unknown versions.
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
let gitHead: string | undefined;
|
|
927
|
+
try {
|
|
928
|
+
gitHead = execFileSync("git", ["rev-parse", "HEAD"], { cwd: sourcePath, encoding: "utf8" }).trim();
|
|
929
|
+
} catch {
|
|
930
|
+
try {
|
|
931
|
+
gitHead = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
932
|
+
} catch {
|
|
933
|
+
gitHead = undefined;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
schemaVersion: 1,
|
|
939
|
+
packageName,
|
|
940
|
+
packageVersion,
|
|
941
|
+
sourcePath,
|
|
942
|
+
gitHead,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
902
946
|
export async function createJob(
|
|
903
947
|
id: string,
|
|
904
948
|
input: OracleSubmitInput,
|
|
@@ -946,6 +990,7 @@ export async function createJob(
|
|
|
946
990
|
originSessionFile: sessionFile,
|
|
947
991
|
requestSource: input.requestSource,
|
|
948
992
|
selection: input.selection,
|
|
993
|
+
extensionProvenance: readExtensionProvenance(cwd),
|
|
949
994
|
followUpToJobId: input.followUpToJobId,
|
|
950
995
|
chatUrl: input.chatUrl,
|
|
951
996
|
conversationId,
|
|
@@ -8,6 +8,7 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
|
|
9
9
|
import { access, cp as copyDirectory, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { delimiter, dirname, join } from "node:path";
|
|
11
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { assertNotKnownBrowserUserDataPath, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
12
13
|
import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
|
|
13
14
|
import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
|
|
@@ -42,8 +43,8 @@ function killProcess(child: ReturnType<typeof spawn>): void {
|
|
|
42
43
|
child.kill("SIGKILL");
|
|
43
44
|
}
|
|
44
45
|
const WORKSPACE_ROOT_MARKERS = [
|
|
45
|
-
"
|
|
46
|
-
|
|
46
|
+
join(CONFIG_DIR_NAME, "extensions", "oracle.json"),
|
|
47
|
+
CONFIG_DIR_NAME,
|
|
47
48
|
"AGENTS.md",
|
|
48
49
|
] as const;
|
|
49
50
|
function cpCommand(): string {
|
|
@@ -677,10 +677,10 @@ function formatOraclePreflightResponse(details: OraclePreflightDetails): string
|
|
|
677
677
|
if (details.ready) {
|
|
678
678
|
return [
|
|
679
679
|
`Oracle preflight ready for ${providerLabel}.`,
|
|
680
|
-
details.session.sessionFile ? `Persisted session: ${details.session.sessionFile}` : undefined,
|
|
681
|
-
details.auth.seedProfileDir ? `Auth seed profile: ${details.auth.seedProfileDir}` : undefined,
|
|
680
|
+
details.session.sessionFile ? `Persisted pi session (current run): ${details.session.sessionFile}` : undefined,
|
|
681
|
+
details.auth.seedProfileDir ? `Auth seed profile (${providerLabel} login source): ${details.auth.seedProfileDir}` : undefined,
|
|
682
682
|
`Preflight validates the persisted pi session, local oracle config, and ${providerLabel} auth seed created by oracle_auth.`,
|
|
683
|
-
"
|
|
683
|
+
"If you are dispatching an oracle job, continue with context gathering and submission.",
|
|
684
684
|
].filter(Boolean).join("\n");
|
|
685
685
|
}
|
|
686
686
|
|
|
@@ -13,10 +13,12 @@ export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: st
|
|
|
13
13
|
export declare function stripChatGptResponseChrome(value: string | undefined): string;
|
|
14
14
|
export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
|
|
15
15
|
export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
|
|
16
|
+
export declare function matchesCompactIntelligenceControlLabel(label: string | undefined): boolean;
|
|
16
17
|
export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
|
|
17
18
|
export declare function requestedEffortLabel(selection: OracleUiSelection): string | undefined;
|
|
18
19
|
export declare function effortSelectionVisible(snapshot: string, effortLabel: string | undefined): boolean;
|
|
19
20
|
export declare function thinkingChipVisible(snapshot: string): boolean;
|
|
21
|
+
export declare function snapshotHasClosedCompactSelection(snapshot: string, selection: OracleUiSelection): boolean;
|
|
20
22
|
export declare function snapshotHasModelConfigurationUi(snapshot: string): boolean;
|
|
21
23
|
export declare function snapshotHasUsableComposerControls(snapshot: string): boolean;
|
|
22
24
|
export declare function snapshotHasModelOpener(snapshot: string): boolean;
|
|
@@ -248,9 +248,29 @@ function hasLegacyEffortCombobox(entries) {
|
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
function compactSelectionFromEntry(entry, _entries,
|
|
252
|
-
if (entry.disabled
|
|
253
|
-
|
|
251
|
+
function compactSelectionFromEntry(entry, _entries, options = {}) {
|
|
252
|
+
if (entry.disabled) return undefined;
|
|
253
|
+
const kind = entry.kind || "";
|
|
254
|
+
if (COMPACT_INTELLIGENCE_CONTROL_KINDS.has(kind)) return parseCompactIntelligenceSelection(entry.label);
|
|
255
|
+
if (options.allowClosedButtons && kind === "button" && !/\bexpanded=true\b/.test(String(entry.line || ""))) {
|
|
256
|
+
return parseCompactIntelligenceSelection(entry.label);
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function matchesCompactIntelligenceControlLabel(label) {
|
|
262
|
+
return Boolean(parseCompactIntelligenceSelection(label));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function snapshotHasClosedCompactSelection(snapshot, selection) {
|
|
266
|
+
/** @type {SnapshotEntry[]} */
|
|
267
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
268
|
+
if (hasRemovableComposerModelChip(entries) || hasLegacyEffortCombobox(entries) || hasCompactIntelligenceMenuContext(entries)) return false;
|
|
269
|
+
return entries.some((entry) => {
|
|
270
|
+
if (entry.kind !== "button" || entry.disabled) return false;
|
|
271
|
+
const compactSelection = compactSelectionFromEntry(entry, entries, { allowClosedButtons: true });
|
|
272
|
+
return compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection);
|
|
273
|
+
});
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
function compactSelectionMatchesRequested(selection, compactSelection) {
|
|
@@ -23,12 +23,14 @@ import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABE
|
|
|
23
23
|
import {
|
|
24
24
|
buildAllowedChatGptOrigins,
|
|
25
25
|
deriveAssistantCompletionSignature,
|
|
26
|
+
matchesCompactIntelligenceControlLabel,
|
|
26
27
|
matchesCompactIntelligenceOpenerLabel,
|
|
27
28
|
matchesModelFamilyLabel,
|
|
28
29
|
matchesRequestedModelControlLabel,
|
|
29
30
|
requestedEffortLabel,
|
|
30
31
|
effortSelectionVisible,
|
|
31
32
|
snapshotCanSafelySkipModelConfiguration,
|
|
33
|
+
snapshotHasClosedCompactSelection,
|
|
32
34
|
snapshotHasModelConfigurationUi,
|
|
33
35
|
snapshotHasModelOpener,
|
|
34
36
|
snapshotHasUsableComposerControls,
|
|
@@ -78,6 +80,7 @@ const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
|
|
|
78
80
|
const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
|
|
79
81
|
const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
|
|
80
82
|
const PROFILE_CLONE_TIMEOUT_MS = 120_000;
|
|
83
|
+
const MODEL_CONFIGURATION_OPEN_TIMEOUT_MS = 45_000;
|
|
81
84
|
const MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS = 20_000;
|
|
82
85
|
const MODEL_CONFIGURATION_SETTLE_POLL_MS = 250;
|
|
83
86
|
const MODEL_CONFIGURATION_CLOSE_RETRY_MS = 1_000;
|
|
@@ -1091,15 +1094,9 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
1091
1094
|
return { state: "challenge_blocking", message: "ChatGPT is showing a challenge/verification page" };
|
|
1092
1095
|
}
|
|
1093
1096
|
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
/an error occurred while connecting to the websocket/i,
|
|
1098
|
-
/try again later/i,
|
|
1099
|
-
/rate limit/i,
|
|
1100
|
-
];
|
|
1101
|
-
if (outagePatterns.some((pattern) => pattern.test(text))) {
|
|
1102
|
-
return { state: "transient_outage_error", message: "ChatGPT is showing a transient outage/error page" };
|
|
1097
|
+
const outageText = detectProviderTransientErrorText(text);
|
|
1098
|
+
if (outageText) {
|
|
1099
|
+
return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/rate-limit page: ${outageText}` };
|
|
1103
1100
|
}
|
|
1104
1101
|
|
|
1105
1102
|
const allowedOrigins = buildAllowedChatGptOrigins(job.config.browser.chatUrl, job.config.browser.authUrl);
|
|
@@ -1162,8 +1159,9 @@ function classifyGrokPage({ url, snapshot, body }) {
|
|
|
1162
1159
|
if (/captcha|cloudflare|verify you are human|unusual activity|suspicious activity/i.test(text)) {
|
|
1163
1160
|
return { state: "challenge_blocking", message: "Grok is showing a challenge/verification page" };
|
|
1164
1161
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1162
|
+
const outageText = detectProviderTransientErrorText(text);
|
|
1163
|
+
if (outageText) {
|
|
1164
|
+
return { state: "transient_outage_error", message: `Grok is showing a transient outage/rate-limit page: ${outageText}` };
|
|
1167
1165
|
}
|
|
1168
1166
|
const onGrokOrigin = typeof url === "string" && url.startsWith("https://grok.com");
|
|
1169
1167
|
if (onGrokOrigin && hasGrokLoginCta(text)) {
|
|
@@ -1250,6 +1248,42 @@ function detectUploadErrorText(text) {
|
|
|
1250
1248
|
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1251
1249
|
}
|
|
1252
1250
|
|
|
1251
|
+
function detectProviderTransientErrorText(text) {
|
|
1252
|
+
const patterns = [
|
|
1253
|
+
"Too many requests",
|
|
1254
|
+
"rate limit",
|
|
1255
|
+
"try again later",
|
|
1256
|
+
"Something went wrong",
|
|
1257
|
+
"A network error occurred",
|
|
1258
|
+
"An error occurred while connecting to the websocket",
|
|
1259
|
+
];
|
|
1260
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function detectProviderVisibleBlockerText(text) {
|
|
1264
|
+
const patterns = [
|
|
1265
|
+
"Too many requests",
|
|
1266
|
+
"rate limit",
|
|
1267
|
+
];
|
|
1268
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function formatProviderTransientErrorMessage(job, errorText, context) {
|
|
1272
|
+
const providerLabel = isGrokJob(job) ? "Grok" : "ChatGPT";
|
|
1273
|
+
return `${providerLabel} is showing a transient outage/rate-limit page${context ? ` while ${context}` : ""}: ${errorText}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function providerTransientErrorMessage(job, text, context) {
|
|
1277
|
+
const errorText = detectProviderVisibleBlockerText(text);
|
|
1278
|
+
if (!errorText) return "";
|
|
1279
|
+
return formatProviderTransientErrorMessage(job, errorText, context);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function throwIfProviderTransientError(job, text, context) {
|
|
1283
|
+
const message = providerTransientErrorMessage(job, text, context);
|
|
1284
|
+
if (message) throw new Error(message);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1253
1287
|
function detectResponseFailureText(text) {
|
|
1254
1288
|
const patterns = [
|
|
1255
1289
|
"Message delivery timed out",
|
|
@@ -1289,6 +1323,7 @@ async function waitForUploadConfirmed(job, fileLabel, baselineCount) {
|
|
|
1289
1323
|
while (Date.now() < timeoutAt) {
|
|
1290
1324
|
await heartbeat();
|
|
1291
1325
|
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1326
|
+
throwIfProviderTransientError(job, snapshot, "uploading the archive");
|
|
1292
1327
|
|
|
1293
1328
|
const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
|
|
1294
1329
|
if (errorText) {
|
|
@@ -1323,6 +1358,7 @@ async function waitForSendReady(job) {
|
|
|
1323
1358
|
await heartbeat();
|
|
1324
1359
|
const snapshot = await snapshotText(job);
|
|
1325
1360
|
const body = await pageText(job).catch(() => "");
|
|
1361
|
+
throwIfProviderTransientError(job, snapshot, "waiting for send readiness");
|
|
1326
1362
|
const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
|
|
1327
1363
|
if (errorText) {
|
|
1328
1364
|
throw new Error(`Upload error detected: ${errorText}`);
|
|
@@ -1366,6 +1402,7 @@ async function sendAcceptanceState(job, baselineAssistantCount) {
|
|
|
1366
1402
|
urlKnown: urlResult.ok,
|
|
1367
1403
|
assistantCount: Math.max(baselineAssistantCount, messages.length),
|
|
1368
1404
|
stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
|
|
1405
|
+
transientErrorText: detectProviderVisibleBlockerText(snapshot) || "",
|
|
1369
1406
|
};
|
|
1370
1407
|
}
|
|
1371
1408
|
|
|
@@ -1386,6 +1423,7 @@ async function waitForSendAccepted(job, beforeSend, options = {}) {
|
|
|
1386
1423
|
while (Date.now() < timeoutAt) {
|
|
1387
1424
|
await heartbeat();
|
|
1388
1425
|
const afterSend = await sendAcceptanceState(job, beforeSend.assistantCount || 0);
|
|
1426
|
+
if (afterSend.transientErrorText) throw new Error(formatProviderTransientErrorMessage(job, afterSend.transientErrorText, "waiting for send acceptance"));
|
|
1389
1427
|
if (providerSendAccepted(beforeSend, afterSend)) return true;
|
|
1390
1428
|
await sleep(500);
|
|
1391
1429
|
}
|
|
@@ -1420,12 +1458,13 @@ async function dismissProFeedbackModal(job, snapshot) {
|
|
|
1420
1458
|
}
|
|
1421
1459
|
|
|
1422
1460
|
async function openModelConfiguration(job) {
|
|
1423
|
-
const timeoutAt = Date.now() +
|
|
1461
|
+
const timeoutAt = Date.now() + MODEL_CONFIGURATION_OPEN_TIMEOUT_MS;
|
|
1424
1462
|
let lastSnapshot = "";
|
|
1425
1463
|
|
|
1426
1464
|
while (Date.now() < timeoutAt) {
|
|
1427
1465
|
const initialSnapshot = await snapshotText(job);
|
|
1428
1466
|
lastSnapshot = initialSnapshot;
|
|
1467
|
+
throwIfProviderTransientError(job, initialSnapshot, "opening model configuration");
|
|
1429
1468
|
if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
|
|
1430
1469
|
if (await dismissProFeedbackModal(job, initialSnapshot)) continue;
|
|
1431
1470
|
|
|
@@ -1438,6 +1477,7 @@ async function openModelConfiguration(job) {
|
|
|
1438
1477
|
await agentBrowser(job, "wait", "800");
|
|
1439
1478
|
const after = await snapshotText(job);
|
|
1440
1479
|
lastSnapshot = after;
|
|
1480
|
+
throwIfProviderTransientError(job, after, "opening model configuration");
|
|
1441
1481
|
if (snapshotHasModelConfigurationUi(after)) return after;
|
|
1442
1482
|
if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
|
|
1443
1483
|
|
|
@@ -1451,6 +1491,7 @@ async function openModelConfiguration(job) {
|
|
|
1451
1491
|
await agentBrowser(job, "wait", "1200");
|
|
1452
1492
|
const postConfigure = await snapshotText(job);
|
|
1453
1493
|
lastSnapshot = postConfigure;
|
|
1494
|
+
throwIfProviderTransientError(job, postConfigure, "opening model configuration");
|
|
1454
1495
|
if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
|
|
1455
1496
|
if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
|
|
1456
1497
|
}
|
|
@@ -1544,22 +1585,28 @@ async function configureModel(job) {
|
|
|
1544
1585
|
throw new Error(`Could not find model family control for ${job.selection.modelFamily}`);
|
|
1545
1586
|
}
|
|
1546
1587
|
|
|
1588
|
+
let compactSelectionVerifiedAfterClick = false;
|
|
1547
1589
|
if (!alreadyConfiguredInUi && !familyAlreadySelectedInUi && familyEntry) {
|
|
1590
|
+
const clickedCompactControl = matchesCompactIntelligenceControlLabel(familyEntry.label);
|
|
1548
1591
|
await clickRef(job, familyEntry.ref);
|
|
1549
1592
|
await agentBrowser(job, "wait", "800");
|
|
1550
1593
|
familySnapshot = await snapshotText(job);
|
|
1551
1594
|
verificationSnapshot = familySnapshot;
|
|
1595
|
+
compactSelectionVerifiedAfterClick = clickedCompactControl && snapshotHasClosedCompactSelection(familySnapshot, job.selection);
|
|
1596
|
+
if (compactSelectionVerifiedAfterClick) {
|
|
1597
|
+
await log(`Verified compact ChatGPT selection after menu close for family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}`);
|
|
1598
|
+
}
|
|
1552
1599
|
const postClickControlOptions = {
|
|
1553
1600
|
ignoreCompactTierButtons: snapshotHasCompactIntelligenceMenuControls(familySnapshot),
|
|
1554
1601
|
ignoreCompactOnlyButtons: snapshotHasLegacyEffortCombobox(familySnapshot),
|
|
1555
1602
|
};
|
|
1556
1603
|
familyEntry = findEntry(familySnapshot, (candidate) => matchesRequestedModelControl(candidate, job.selection, postClickControlOptions));
|
|
1557
|
-
if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
|
|
1604
|
+
if (!compactSelectionVerifiedAfterClick && !familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
|
|
1558
1605
|
throw new Error(`Requested model family did not remain selected: ${job.selection.modelFamily}`);
|
|
1559
1606
|
}
|
|
1560
1607
|
}
|
|
1561
1608
|
|
|
1562
|
-
if (job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") {
|
|
1609
|
+
if ((job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") && !compactSelectionVerifiedAfterClick) {
|
|
1563
1610
|
const effortLabel = requestedEffortLabel(job.selection);
|
|
1564
1611
|
if (effortLabel && !effortSelectionVisible(familySnapshot, effortLabel)) {
|
|
1565
1612
|
const opened = await openEffortDropdown(job);
|
|
@@ -1589,7 +1636,8 @@ async function configureModel(job) {
|
|
|
1589
1636
|
if (job.selection.modelFamily === "instant") {
|
|
1590
1637
|
const desiredAutoSwitchState = job.selection.autoSwitchToThinking === true;
|
|
1591
1638
|
const currentAutoSwitchState = autoSwitchToThinkingSelectionVisible(familySnapshot);
|
|
1592
|
-
const compactInstantAlreadyVerified =
|
|
1639
|
+
const compactInstantAlreadyVerified = compactSelectionVerifiedAfterClick
|
|
1640
|
+
|| (desiredAutoSwitchState && currentAutoSwitchState === undefined && snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection));
|
|
1593
1641
|
if (!compactInstantAlreadyVerified && currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
|
|
1594
1642
|
await clickAutoSwitchToThinkingControl(job);
|
|
1595
1643
|
await agentBrowser(job, "wait", "400");
|
|
@@ -1598,7 +1646,7 @@ async function configureModel(job) {
|
|
|
1598
1646
|
}
|
|
1599
1647
|
}
|
|
1600
1648
|
|
|
1601
|
-
const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
|
|
1649
|
+
const stronglyVerified = compactSelectionVerifiedAfterClick || snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
|
|
1602
1650
|
if (!stronglyVerified) {
|
|
1603
1651
|
throw new Error(`Could not verify requested model settings in configuration UI for ${job.selection.modelFamily}`);
|
|
1604
1652
|
}
|
|
@@ -1742,12 +1790,15 @@ async function grokAssistantMessages(job) {
|
|
|
1742
1790
|
return text;
|
|
1743
1791
|
};
|
|
1744
1792
|
const bubbles = Array.from(document.querySelectorAll('.message-bubble'));
|
|
1793
|
+
const roleMessages = Array.from(document.querySelectorAll('[data-message-author-role="assistant"]'));
|
|
1745
1794
|
const sourceNodes = bubbles.length > 0
|
|
1746
1795
|
? bubbles
|
|
1747
|
-
:
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1796
|
+
: roleMessages.length > 0
|
|
1797
|
+
? roleMessages
|
|
1798
|
+
: Array.from(document.querySelectorAll('div')).filter((node) => {
|
|
1799
|
+
const classText = String(node.className || '');
|
|
1800
|
+
return classText.includes('group') && classText.includes('flex') && classText.includes('flex-col') && classText.includes('justify-center');
|
|
1801
|
+
});
|
|
1751
1802
|
const messages = sourceNodes
|
|
1752
1803
|
.map((node) => node.closest('[data-message-author-role], [data-testid*="message"], .group') || node)
|
|
1753
1804
|
.filter((node, index, all) => all.indexOf(node) === index)
|
|
@@ -1793,6 +1844,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1793
1844
|
const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming");
|
|
1794
1845
|
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1795
1846
|
const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
|
|
1847
|
+
throwIfProviderTransientError(job, snapshot, "waiting for response completion");
|
|
1796
1848
|
const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
|
|
1797
1849
|
const messages = await assistantMessages(job);
|
|
1798
1850
|
const targetMessage = messages[baselineAssistantCount];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.14",
|
|
4
4
|
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"platform-smoke.config.mjs",
|
|
37
37
|
"scripts/platform-smoke.mjs",
|
|
38
38
|
"scripts/platform-smoke",
|
|
39
|
-
"scripts/oracle-real-smoke.mjs"
|
|
39
|
+
"scripts/oracle-real-smoke.mjs",
|
|
40
|
+
"scripts/oracle-chatgpt-preset-proof.mjs"
|
|
40
41
|
],
|
|
41
42
|
"pi": {
|
|
42
43
|
"extensions": [
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
|
|
50
51
|
"sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
|
|
51
52
|
"pack:check": "npm pack --dry-run",
|
|
52
|
-
"verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
|
|
53
|
+
"verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
|
|
53
54
|
"test": "npm run verify:oracle",
|
|
54
55
|
"prepublishOnly": "npm run release:check",
|
|
55
56
|
"check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/targets.mjs && node scripts/platform-smoke/invariants.mjs",
|
|
@@ -61,12 +62,14 @@
|
|
|
61
62
|
"smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
|
|
62
63
|
"smoke:real": "npm run smoke:real:packed",
|
|
63
64
|
"smoke:real:doctor": "node scripts/oracle-real-smoke.mjs doctor",
|
|
64
|
-
"release:check": "npm run verify:oracle && npm run smoke:platform:all",
|
|
65
|
+
"release:check": "npm run verify:oracle && npm run release:proof:chatgpt-presets && npm run smoke:platform:all",
|
|
65
66
|
"check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs",
|
|
67
|
+
"check:oracle-release-proof": "node --check scripts/oracle-chatgpt-preset-proof.mjs",
|
|
68
|
+
"release:proof:chatgpt-presets": "node scripts/oracle-chatgpt-preset-proof.mjs check",
|
|
66
69
|
"smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
|
|
67
70
|
"smoke:real:source": "node scripts/oracle-real-smoke.mjs run --mode source",
|
|
68
71
|
"sanity:oracle:platform": "node scripts/oracle-sanity-runner.mjs --mode platform",
|
|
69
|
-
"verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run sanity:oracle:platform && npm run pack:check"
|
|
72
|
+
"verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run sanity:oracle:platform && npm run pack:check"
|
|
70
73
|
},
|
|
71
74
|
"dependencies": {
|
|
72
75
|
"@steipete/sweet-cookie": "^0.3.0"
|
|
@@ -80,8 +83,8 @@
|
|
|
80
83
|
"protobufjs": "7.6.1"
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
83
|
-
"@earendil-works/pi-ai": "0.79.
|
|
84
|
-
"@earendil-works/pi-coding-agent": "0.79.
|
|
86
|
+
"@earendil-works/pi-ai": "^0.79.10",
|
|
87
|
+
"@earendil-works/pi-coding-agent": "^0.79.10",
|
|
85
88
|
"@types/node": "^22.19.19",
|
|
86
89
|
"esbuild": "^0.28.0",
|
|
87
90
|
"tsx": "^4.22.3",
|
|
@@ -23,7 +23,7 @@ export default {
|
|
|
23
23
|
commands: ["npm run smoke:platform:all"],
|
|
24
24
|
},
|
|
25
25
|
release: {
|
|
26
|
-
description: "Full release gate: local verification plus the doctor-first platform matrix.",
|
|
26
|
+
description: "Full release gate: local verification, fresh ChatGPT preset proof, plus the doctor-first platform matrix.",
|
|
27
27
|
commands: ["npm run release:check"],
|
|
28
28
|
},
|
|
29
29
|
},
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Purpose: Release-blocking proof gate for live ChatGPT preset selection.
|
|
3
|
+
// Responsibilities: Validate that a fresh manual/live oracle job matrix covered every canonical ChatGPT preset before publish.
|
|
4
|
+
// Scope: Maintainer release safety only; the script does not submit jobs or touch provider accounts.
|
|
5
|
+
// Usage: npm run release:proof:chatgpt-presets, or `node scripts/oracle-chatgpt-preset-proof.mjs template`.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, "..");
|
|
14
|
+
const DEFAULT_PROOF_PATH = ".artifacts/chatgpt-preset-proof/latest.json";
|
|
15
|
+
const PROOF_PATH_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF";
|
|
16
|
+
const JOBS_DIR_ENV = "PI_ORACLE_JOBS_DIR";
|
|
17
|
+
const MAX_AGE_HOURS_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF_MAX_AGE_HOURS";
|
|
18
|
+
const DEFAULT_MAX_AGE_HOURS = 72;
|
|
19
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
21
|
+
|
|
22
|
+
function usage() {
|
|
23
|
+
console.log(`Usage: node scripts/oracle-chatgpt-preset-proof.mjs <check|template>
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
check Validate release-blocking live ChatGPT preset proof. Default.
|
|
27
|
+
template Print a non-valid proof-file template for the current package version/git head.
|
|
28
|
+
|
|
29
|
+
Environment:
|
|
30
|
+
${PROOF_PATH_ENV} Proof JSON path (default: ${DEFAULT_PROOF_PATH})
|
|
31
|
+
${JOBS_DIR_ENV} Oracle jobs root for job lookup (default also checks /tmp)
|
|
32
|
+
${MAX_AGE_HOURS_ENV} Freshness window in hours (default: ${DEFAULT_MAX_AGE_HOURS})
|
|
33
|
+
|
|
34
|
+
Proof file contract:
|
|
35
|
+
The proof must reference live oracle job state produced by the loaded extension
|
|
36
|
+
after the current git HEAD. It must include one completed ChatGPT job per
|
|
37
|
+
canonical ORACLE_SUBMIT_PRESETS id. Shape-only proof is rejected.
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fail(message) {
|
|
42
|
+
console.error(message);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(path) {
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function git(args) {
|
|
51
|
+
return execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf8" }).trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packageMetadata() {
|
|
55
|
+
const pkg = readJson(resolve(REPO_ROOT, "package.json"));
|
|
56
|
+
return { name: pkg.name, version: pkg.version };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function currentGitHead() {
|
|
60
|
+
return git(["rev-parse", "HEAD"]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentGitHeadCommittedAt() {
|
|
64
|
+
return git(["show", "-s", "--format=%cI", "HEAD"]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function currentGitStatus() {
|
|
68
|
+
return git(["status", "--short"]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalPresets() {
|
|
72
|
+
const configSource = readFileSync(resolve(REPO_ROOT, "extensions/oracle/lib/config.ts"), "utf8");
|
|
73
|
+
const registryMatch = configSource.match(/export const ORACLE_SUBMIT_PRESETS = \{([\s\S]*?)\n\} as const;/);
|
|
74
|
+
if (!registryMatch) throw new Error("Could not locate ORACLE_SUBMIT_PRESETS registry in extensions/oracle/lib/config.ts");
|
|
75
|
+
const entries = [...registryMatch[1].matchAll(
|
|
76
|
+
/^\s{2}([a-z0-9_]+):\s*\{\s*label:\s*"[^"]+",\s*modelFamily:\s*"([a-z]+)"\s+as const(?:,\s*effort:\s*"([a-z]+)"\s+as const)?,\s*autoSwitchToThinking:\s*(true|false)\s*\}/gm,
|
|
77
|
+
)];
|
|
78
|
+
if (entries.length === 0) throw new Error("Could not parse ORACLE_SUBMIT_PRESETS registry entries");
|
|
79
|
+
return Object.fromEntries(entries.map((match) => [match[1], {
|
|
80
|
+
modelFamily: match[2],
|
|
81
|
+
effort: match[3],
|
|
82
|
+
autoSwitchToThinking: match[4] === "true",
|
|
83
|
+
}]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function canonicalPresetIds() {
|
|
87
|
+
return Object.keys(canonicalPresets());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function proofPath() {
|
|
91
|
+
return resolve(REPO_ROOT, process.env[PROOF_PATH_ENV] || DEFAULT_PROOF_PATH);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function maxAgeHours() {
|
|
95
|
+
const raw = process.env[MAX_AGE_HOURS_ENV];
|
|
96
|
+
if (!raw) return DEFAULT_MAX_AGE_HOURS;
|
|
97
|
+
const parsed = Number(raw);
|
|
98
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${MAX_AGE_HOURS_ENV} must be a positive number of hours`);
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isIsoDate(value) {
|
|
103
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
104
|
+
const millis = Date.parse(value);
|
|
105
|
+
return Number.isFinite(millis) && new Date(millis).toISOString() === value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseIsoMillis(value) {
|
|
109
|
+
return isIsoDate(value) ? Date.parse(value) : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unique(values) {
|
|
113
|
+
return [...new Set(values.filter(Boolean))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function candidateJobJsonPaths(jobId, proofJob) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
if (typeof proofJob.jobJsonPath === "string" && proofJob.jobJsonPath.trim()) {
|
|
119
|
+
paths.push(resolve(REPO_ROOT, proofJob.jobJsonPath));
|
|
120
|
+
}
|
|
121
|
+
if (typeof proofJob.jobDir === "string" && proofJob.jobDir.trim()) {
|
|
122
|
+
paths.push(resolve(REPO_ROOT, proofJob.jobDir, "job.json"));
|
|
123
|
+
}
|
|
124
|
+
if (process.env[JOBS_DIR_ENV]) {
|
|
125
|
+
paths.push(resolve(process.env[JOBS_DIR_ENV], `oracle-${jobId}`, "job.json"));
|
|
126
|
+
}
|
|
127
|
+
paths.push(resolve("/tmp", `oracle-${jobId}`, "job.json"));
|
|
128
|
+
return unique(paths);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function loadOracleJobState(jobId, proofJob) {
|
|
132
|
+
const candidates = candidateJobJsonPaths(jobId, proofJob);
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (!existsSync(candidate)) continue;
|
|
135
|
+
return { path: candidate, state: readJson(candidate) };
|
|
136
|
+
}
|
|
137
|
+
return { path: undefined, state: undefined, candidates };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireActualJobEvidence({ preset, canonicalPreset, proofJob, packageName, packageVersion, gitHead, gitHeadCommittedAtMs, proofValidatedAtMs, errors }) {
|
|
141
|
+
if (!proofJob || typeof proofJob !== "object" || Array.isArray(proofJob)) {
|
|
142
|
+
errors.push(`missing jobs.${preset}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (proofJob.preset !== preset) errors.push(`jobs.${preset}.preset must be ${preset}`);
|
|
146
|
+
if (proofJob.provider !== "chatgpt") errors.push(`jobs.${preset}.provider must be chatgpt`);
|
|
147
|
+
|
|
148
|
+
const jobId = proofJob.jobId;
|
|
149
|
+
if (typeof jobId !== "string" || !UUID_PATTERN.test(jobId) || jobId === ZERO_UUID) {
|
|
150
|
+
errors.push(`jobs.${preset}.jobId must be a real oracle UUID job id, not a placeholder`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const loaded = loadOracleJobState(jobId, proofJob);
|
|
155
|
+
if (!loaded.state) {
|
|
156
|
+
errors.push(`jobs.${preset} could not find actual oracle job.json for ${jobId}; checked ${loaded.candidates.join(", ")}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const state = loaded.state;
|
|
161
|
+
const responsePath = typeof state.responsePath === "string" ? state.responsePath : undefined;
|
|
162
|
+
const workerLogPath = typeof state.workerLogPath === "string" ? state.workerLogPath : undefined;
|
|
163
|
+
const response = responsePath && existsSync(responsePath) ? readFileSync(responsePath, "utf8") : "";
|
|
164
|
+
const workerLog = workerLogPath && existsSync(workerLogPath) ? readFileSync(workerLogPath, "utf8") : "";
|
|
165
|
+
const completedAtMs = parseIsoMillis(state.completedAt || state.phaseAt);
|
|
166
|
+
|
|
167
|
+
if (state.id !== jobId) errors.push(`jobs.${preset} job.json id mismatch: expected ${jobId}, got ${state.id || "<missing>"}`);
|
|
168
|
+
if (state.status !== "complete") errors.push(`jobs.${preset} actual job status must be complete, got ${state.status || "<missing>"}`);
|
|
169
|
+
if (state.phase !== "complete") errors.push(`jobs.${preset} actual job phase must be complete, got ${state.phase || "<missing>"}`);
|
|
170
|
+
if (state.selection?.provider !== "chatgpt") errors.push(`jobs.${preset} actual provider must be chatgpt`);
|
|
171
|
+
if (state.selection?.preset !== preset) errors.push(`jobs.${preset} actual preset must be ${preset}, got ${state.selection?.preset || "<missing>"}`);
|
|
172
|
+
if (state.selection?.modelFamily !== canonicalPreset.modelFamily) errors.push(`jobs.${preset} actual modelFamily must be ${canonicalPreset.modelFamily}, got ${state.selection?.modelFamily || "<missing>"}`);
|
|
173
|
+
if ((state.selection?.effort || undefined) !== canonicalPreset.effort) errors.push(`jobs.${preset} actual effort must be ${canonicalPreset.effort || "<unset>"}, got ${state.selection?.effort || "<unset>"}`);
|
|
174
|
+
if (state.selection?.autoSwitchToThinking !== canonicalPreset.autoSwitchToThinking) errors.push(`jobs.${preset} actual autoSwitchToThinking must be ${canonicalPreset.autoSwitchToThinking}`);
|
|
175
|
+
if (state.cwd !== REPO_ROOT) errors.push(`jobs.${preset} actual cwd must be this repo (${REPO_ROOT}), got ${state.cwd || "<missing>"}`);
|
|
176
|
+
if (state.projectId !== REPO_ROOT) errors.push(`jobs.${preset} actual projectId must be this repo (${REPO_ROOT}), got ${state.projectId || "<missing>"}`);
|
|
177
|
+
if (state.requestSource !== "tool" && state.requestSource !== "command") errors.push(`jobs.${preset} actual requestSource must be tool or command`);
|
|
178
|
+
if (typeof state.sessionId !== "string" || !state.sessionId.trim()) errors.push(`jobs.${preset} actual job must record sessionId`);
|
|
179
|
+
if (typeof state.originSessionFile !== "string" || !existsSync(state.originSessionFile)) errors.push(`jobs.${preset} actual originSessionFile must exist`);
|
|
180
|
+
if (typeof state.promptPath !== "string" || !existsSync(state.promptPath)) errors.push(`jobs.${preset} actual promptPath must exist`);
|
|
181
|
+
if (typeof state.logsDir !== "string" || !existsSync(state.logsDir)) errors.push(`jobs.${preset} actual logsDir must exist`);
|
|
182
|
+
if (typeof state.runtimeId !== "string" || !state.runtimeId.trim()) errors.push(`jobs.${preset} actual job must record runtimeId`);
|
|
183
|
+
if (typeof state.runtimeSessionName !== "string" || !state.runtimeSessionName.trim()) errors.push(`jobs.${preset} actual job must record runtimeSessionName`);
|
|
184
|
+
if (!state.config?.browser || !state.config?.worker || !state.config?.cleanup) errors.push(`jobs.${preset} actual job must include persisted oracle config with browser, worker, and cleanup sections`);
|
|
185
|
+
const lifecycleKinds = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.kind) : []);
|
|
186
|
+
const lifecyclePhases = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.phase) : []);
|
|
187
|
+
if (!lifecycleKinds.has("created")) errors.push(`jobs.${preset} lifecycle events must include job creation`);
|
|
188
|
+
if (!lifecyclePhases.has("configuring_model")) errors.push(`jobs.${preset} lifecycle events must include configuring_model phase`);
|
|
189
|
+
if (!lifecyclePhases.has("complete")) errors.push(`jobs.${preset} lifecycle events must include complete phase`);
|
|
190
|
+
if (state.extensionProvenance?.schemaVersion !== 1) errors.push(`jobs.${preset} actual job must record extensionProvenance.schemaVersion=1`);
|
|
191
|
+
if (state.extensionProvenance?.packageName !== packageName) errors.push(`jobs.${preset} actual extension packageName must be ${packageName}`);
|
|
192
|
+
if (state.extensionProvenance?.packageVersion !== packageVersion) errors.push(`jobs.${preset} actual extension packageVersion must be ${packageVersion}`);
|
|
193
|
+
if (state.extensionProvenance?.gitHead !== gitHead) errors.push(`jobs.${preset} actual extension gitHead must be ${gitHead}`);
|
|
194
|
+
if (state.extensionProvenance?.sourcePath !== REPO_ROOT) errors.push(`jobs.${preset} actual extension sourcePath must be this repo (${REPO_ROOT}), got ${state.extensionProvenance?.sourcePath || "<missing>"}`);
|
|
195
|
+
if (typeof state.archivePath !== "string" || !state.archivePath.endsWith(".tar.zst")) errors.push(`jobs.${preset} actual archivePath must end with .tar.zst`);
|
|
196
|
+
if (typeof state.archiveSha256 !== "string" || !/^[0-9a-f]{64}$/i.test(state.archiveSha256)) errors.push(`jobs.${preset} actual job must record archiveSha256`);
|
|
197
|
+
if (typeof state.conversationId !== "string" || !state.conversationId.trim()) errors.push(`jobs.${preset} actual job must record conversationId`);
|
|
198
|
+
if (typeof state.chatUrl !== "string" || !state.chatUrl.startsWith("https://chatgpt.com/c/")) errors.push(`jobs.${preset} actual job must record a ChatGPT conversation URL`);
|
|
199
|
+
if (!responsePath || !existsSync(responsePath)) errors.push(`jobs.${preset} actual responsePath must exist`);
|
|
200
|
+
if (!workerLogPath || !existsSync(workerLogPath)) errors.push(`jobs.${preset} actual workerLogPath must exist`);
|
|
201
|
+
if (!response.includes(`PRESET ${preset} OK`)) errors.push(`jobs.${preset} actual response must include PRESET ${preset} OK`);
|
|
202
|
+
if (!response.includes(`PACKAGE ${packageName}`)) errors.push(`jobs.${preset} actual response must include PACKAGE ${packageName}`);
|
|
203
|
+
if (!workerLog.includes(`Configuring model family=${state.selection?.modelFamily}`) && !workerLog.includes("Model already appears configured")) {
|
|
204
|
+
errors.push(`jobs.${preset} worker log must show model configuration or an explicit already-configured skip`);
|
|
205
|
+
}
|
|
206
|
+
if (!workerLog.includes("Job completed successfully") && !workerLog.includes(`Job ${jobId} complete`)) errors.push(`jobs.${preset} worker log must show successful completion`);
|
|
207
|
+
|
|
208
|
+
if (completedAtMs === undefined) {
|
|
209
|
+
errors.push(`jobs.${preset} actual completedAt/phaseAt must be an ISO timestamp`);
|
|
210
|
+
} else {
|
|
211
|
+
if (completedAtMs <= gitHeadCommittedAtMs) errors.push(`jobs.${preset} must complete after current git HEAD commit time`);
|
|
212
|
+
if (proofValidatedAtMs !== undefined && completedAtMs > proofValidatedAtMs) errors.push(`jobs.${preset} completed after proof validatedAt`);
|
|
213
|
+
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
214
|
+
if (Date.now() - completedAtMs > maxAgeMs) errors.push(`jobs.${preset} completedAt is older than ${maxAgeHours()} hours`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof proofJob.conversation === "string" && proofJob.conversation.trim() && proofJob.conversation !== state.conversationId && proofJob.conversation !== state.chatUrl) {
|
|
218
|
+
errors.push(`jobs.${preset}.conversation does not match actual conversationId/chatUrl`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateProof(proof, path) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
const { name, version } = packageMetadata();
|
|
225
|
+
const gitHead = currentGitHead();
|
|
226
|
+
const gitHeadCommittedAt = currentGitHeadCommittedAt();
|
|
227
|
+
const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
|
|
228
|
+
const gitStatus = currentGitStatus();
|
|
229
|
+
const presetRegistry = canonicalPresets();
|
|
230
|
+
const requiredPresets = Object.keys(presetRegistry);
|
|
231
|
+
const allowedPresets = new Set(requiredPresets);
|
|
232
|
+
|
|
233
|
+
if (gitStatus) {
|
|
234
|
+
errors.push(`working tree must be clean before release proof is accepted; current changes:\n${gitStatus}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!proof || typeof proof !== "object" || Array.isArray(proof)) {
|
|
238
|
+
errors.push("proof root must be a JSON object");
|
|
239
|
+
return errors;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (proof.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
243
|
+
if (proof.packageName !== name) errors.push(`packageName must be ${name}`);
|
|
244
|
+
if (proof.packageVersion !== version) errors.push(`packageVersion must match package.json version ${version}`);
|
|
245
|
+
if (proof.gitHead !== gitHead) errors.push(`gitHead must match current HEAD ${gitHead}`);
|
|
246
|
+
if (proof.provider !== "chatgpt") errors.push('provider must be "chatgpt"');
|
|
247
|
+
if (proof.extensionUnderTest !== "loaded-extension") errors.push('extensionUnderTest must be "loaded-extension"');
|
|
248
|
+
|
|
249
|
+
let proofValidatedAtMs;
|
|
250
|
+
if (!isIsoDate(proof.validatedAt)) {
|
|
251
|
+
errors.push("validatedAt must be an ISO-8601 UTC timestamp from new Date().toISOString()");
|
|
252
|
+
} else {
|
|
253
|
+
proofValidatedAtMs = Date.parse(proof.validatedAt);
|
|
254
|
+
const ageMs = Date.now() - proofValidatedAtMs;
|
|
255
|
+
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
256
|
+
if (ageMs < 0) errors.push("validatedAt must not be in the future");
|
|
257
|
+
if (ageMs > maxAgeMs) errors.push(`validatedAt is older than ${maxAgeHours()} hours`);
|
|
258
|
+
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after current git HEAD commit time");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const jobs = proof.jobs;
|
|
262
|
+
if (!jobs || typeof jobs !== "object" || Array.isArray(jobs)) {
|
|
263
|
+
errors.push("jobs must be an object keyed by canonical preset id");
|
|
264
|
+
return errors;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const preset of requiredPresets) {
|
|
268
|
+
requireActualJobEvidence({
|
|
269
|
+
preset,
|
|
270
|
+
canonicalPreset: presetRegistry[preset],
|
|
271
|
+
proofJob: jobs[preset],
|
|
272
|
+
packageName: name,
|
|
273
|
+
packageVersion: version,
|
|
274
|
+
gitHead,
|
|
275
|
+
gitHeadCommittedAtMs,
|
|
276
|
+
proofValidatedAtMs,
|
|
277
|
+
errors,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const preset of Object.keys(jobs)) {
|
|
282
|
+
if (!allowedPresets.has(preset)) errors.push(`jobs.${preset} is not a canonical ORACLE_SUBMIT_PRESETS id`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (errors.length === 0) {
|
|
286
|
+
console.log(`ChatGPT preset release proof accepted: ${path}`);
|
|
287
|
+
console.log(`Validated presets: ${requiredPresets.join(", ")}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return errors;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function template() {
|
|
294
|
+
const { name, version } = packageMetadata();
|
|
295
|
+
const gitHead = currentGitHead();
|
|
296
|
+
const jobs = Object.fromEntries(canonicalPresetIds().map((preset) => [preset, {
|
|
297
|
+
preset,
|
|
298
|
+
provider: "chatgpt",
|
|
299
|
+
jobId: `replace-with-completed-${preset}-job-uuid`,
|
|
300
|
+
jobDir: `/tmp/oracle-replace-with-completed-${preset}-job-uuid`,
|
|
301
|
+
conversation: "replace-with-actual-conversation-id-or-chat-url",
|
|
302
|
+
}]));
|
|
303
|
+
|
|
304
|
+
console.log(JSON.stringify({
|
|
305
|
+
schemaVersion: 1,
|
|
306
|
+
packageName: name,
|
|
307
|
+
packageVersion: version,
|
|
308
|
+
gitHead,
|
|
309
|
+
provider: "chatgpt",
|
|
310
|
+
extensionUnderTest: "loaded-extension",
|
|
311
|
+
validatedAt: new Date().toISOString(),
|
|
312
|
+
jobs,
|
|
313
|
+
}, null, 2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function main() {
|
|
317
|
+
const command = process.argv[2] || "check";
|
|
318
|
+
if (command === "--help" || command === "-h") {
|
|
319
|
+
usage();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (command === "template") {
|
|
323
|
+
template();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (command !== "check") {
|
|
327
|
+
usage();
|
|
328
|
+
fail(`Unknown command: ${command}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const path = proofPath();
|
|
333
|
+
if (!existsSync(path)) {
|
|
334
|
+
fail(`Missing ChatGPT preset release proof: ${path}\n\nRun live loaded-extension oracle jobs for every canonical ChatGPT preset, then save proof JSON.\nCreate a non-valid starting template with:\n mkdir -p .artifacts/chatgpt-preset-proof\n node scripts/oracle-chatgpt-preset-proof.mjs template > ${DEFAULT_PROOF_PATH}\n\nThis gate is intentional: releases are blocked until every preset has fresh live proof backed by actual oracle job state.`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let proof;
|
|
339
|
+
try {
|
|
340
|
+
proof = readJson(path);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
fail(`Could not read proof JSON at ${path}: ${error.message}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const errors = validateProof(proof, path);
|
|
347
|
+
if (errors.length > 0) {
|
|
348
|
+
fail(`ChatGPT preset release proof rejected: ${path}\n- ${errors.join("\n- ")}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main();
|
|
@@ -92,7 +92,7 @@ function testCanonicalWorkflowConfig() {
|
|
|
92
92
|
assert.deepEqual(config.workflows?.release?.commands, ["npm run release:check"], "release workflow should use the full local-plus-platform release gate");
|
|
93
93
|
assert.equal(config.requiredCrabbox?.minVersion, "0.26.0", "Crabbox baseline should match the documented provider contract");
|
|
94
94
|
assert.equal(pkg.scripts["smoke:platform:all"], "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native", "full platform smoke should remain doctor-first and cover all required targets");
|
|
95
|
-
assert.match(pkg.scripts["release:check"], /npm run verify:oracle && npm run smoke:platform:all/, "release check should combine local verification and full platform smoke");
|
|
95
|
+
assert.match(pkg.scripts["release:check"], /npm run verify:oracle && npm run release:proof:chatgpt-presets && npm run smoke:platform:all/, "release check should combine local verification, ChatGPT preset proof, and full platform smoke");
|
|
96
96
|
const runnerSource = readFileSync(new URL("./crabbox-runner.mjs", import.meta.url), "utf8");
|
|
97
97
|
assert.match(runnerSource, /PLATFORM_SMOKE_CRABBOX/, "runner should honor reusable Crabbox binary override");
|
|
98
98
|
assert.match(runnerSource, /PLATFORM_SMOKE_MAC_WORK_ROOT/, "runner should honor reusable macOS work-root override");
|