diff --git a/.project-docs/30-worklog/tasks/20260822-pi-runtime-qualification-c52e8a14.md b/.project-docs/30-worklog/tasks/20260822-pi-runtime-qualification-c52e8a14.md new file mode 100644 index 0000000..3c3dc98 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260822-pi-runtime-qualification-c52e8a14.md @@ -0,0 +1,79 @@ +# Task: Implement PI-000 Pi runtime qualification gate + +## Identity + +- Task ID: 20260822-pi-runtime-qualification-c52e8a14 +- Mode: Feature +- Branch: codex/20260822-pi-runtime-qualification-c52e8a14-pi-runtime-qualification +- Worktree: D:\Datas\OthersProjects\makelore-pi-runtime-qualification-c52e8a14 +- Base commit: fba68e86d93c66d2c50f53d215de76a7c69c97a7 +- Owner: codex +- Status: Completed — Done with explicit waivers + +## Scope + +- Implement the `PI-000 Runtime qualification gate` defined by the accepted Pi runtime cutover specification. +- Pin the selected Pi runtime package exactly and keep the frozen lockfile reproducible. +- Add a minimal, standalone RPC qualification probe and focused tests for runtime discovery, JSONL framing, session reopen, abort/settled handling, dependency closure, process cleanup, and measurement reporting. +- Produce task-scoped evidence for the supported platform/artifact/provider samples that can actually be run from this worktree. +- Do not change Chat, Host API routes, Renderer behavior, OpenCode migration paths, or final packaged runtime wiring in this task. + +## Intent And Constraints + +- Candidate runtime: `@earendil-works/pi-coding-agent@0.84.2`, exact version only. Its npm package, CLI entry, Node engine, upstream tag, and commit must agree before it is accepted. +- The intended packaged seam is Electron's executable with `ELECTRON_RUN_AS_NODE=1` and Pi's RPC CLI; the gate must disprove this seam if packaging, concurrency, provider compatibility, resource discovery, or latency budgets fail. +- Required protocol coverage is the current four-protocol Makelore contract: `openai-completions`, `openai-responses`, `anthropic-messages`, and `openrouter`, including base URL, headers, credential injection, model selection, and image-input capability where applicable. +- Windows/Linux and credential-free samples establish the Phase-0 platform evidence. They do not validate real-provider compatibility, concurrency, abort isolation, credential/model isolation, or image-input behavior; the user explicitly waived those remaining `QG-004`/`QG-005` checks on 2026-08-22 and accepted the resulting risk. +- The user explicitly waived macOS x64/arm64 from PI-000 on 2026-08-22 and accepted the risk of discovering macOS packaging/native/resource/performance defects later. This is a deferral to PI-150, not a macOS Pass or removal of final cross-platform release validation. +- The real Provider waiver is not a Pass. Real authentication, endpoint/proxy/rate-limit/provider variation, base URL/header/environment credential/model/image compatibility, true provider concurrency or serialization, and cross-worker abort/event/session/model/credential isolation remain unverified. +- No OpenCode fallback, compatibility layer, RPC/SDK dual path, speculative migration framework, or unrelated refactor is allowed. +- If this gate fails, implementation stops at PI-000 and the planner must amend the specification/ADR to the SDK-in-Electron-utility-process alternative. + +## Outcome + +- Implemented the exact Pi dependency pin, frozen lockfile update, standalone qualification probe, temporary production-closure staging, controlled Electron Builder `app.asar` probe, real-provider fixture path, four-protocol loopback contract probe, and focused unit coverage. +- Corrected the controlled packaged-closure inspector before macOS execution: the packaged resource directory is now computed once and passed into the artifact process, so a macOS executable reads `Contents/Resources/app.asar` instead of the invalid `Contents/MacOS/resources/app.asar` path. +- Windows x64 and WSL2 Ubuntu 24.04 Linux x64 workspace, staged, and controlled packaged samples are a partial pass. Runtime identity, JSONL RPC, session persistence/reopen, deterministic failure settlement, local worker isolation, shell abort isolation, complete artifact dependency/resource resolution, startup budgets, RSS reporting, and clean exit were confirmed. +- Windows and Linux packaged loopback samples passed request/SSE/image/header/credential/model contracts for `openai-completions`, `openai-responses`, `anthropic-messages`, and `openrouter`, including two-worker overlap and one-worker abort isolation. This is not real Provider Account evidence. +- The planner audited every PI-000 Required work, Acceptance item, and stop condition after the two user decisions. No unwaived Phase-0 item remains incomplete and no confirmed failure exists, so PI-000 is **Done with explicit waivers**, not an all-gates Pass. The new Ready Frontier is `PI-010`, `PI-020`, and `PI-030`. + +## Verification + +- Project-docs preflight and concurrent/planning gates passed in this isolated feature worktree. +- `@earendil-works/pi-coding-agent@0.84.2` npm metadata and upstream `v0.84.2` tag both resolve to commit `914cf1472e715297caa30db4b9535d534a9eb718`. +- Electron `43.4.0` runs Node `24.18.1` through `ELECTRON_RUN_AS_NODE=1`, satisfying Pi's declared Node `>=22.19.0` engine floor. +- `pnpm exec vitest run tests/unit/pi-runtime-probe.test.ts`: 9 tests passed, including macOS packaged-resource path coverage, aggregate-evidence wording, and explicit real-Provider waiver reporting. +- `pnpm exec eslint scripts/probe-pi-runtime.mjs scripts/probe-pi-packaged-runtime.mjs scripts/probe-pi-provider-contracts.mjs tests/unit/pi-runtime-probe.test.ts`: passed. +- `pnpm run typecheck`: passed. +- `pnpm run build:vite`: passed; existing chunk-size and mixed static/dynamic import warnings remain unchanged and are outside PI-000. +- `node scripts/probe-pi-runtime.mjs --stage --samples 5 --timeout-ms 10000`: partial pass on Windows x64. Cold ready p95 681 ms, warm ready p95 674 ms, RSS p95 125780 KiB, exit p95 16 ms; 130 applicable shrinkwrap packages present and 9 platform-specific entries skipped. +- `node scripts/probe-pi-packaged-runtime.mjs --samples 5 --timeout-ms 10000`: partial pass on a controlled Windows x64 Electron Builder `app.asar` artifact. Cold ready p95 682 ms, warm ready p95 663 ms, RSS p95 141320 KiB, exit p95 18 ms; all 130 applicable packages and six runtime assets resolve from the artifact, with one same-version dependency flattened by Electron Builder. +- `node scripts/probe-pi-provider-contracts.mjs ...packaged-runtime-paths...`: passed all four loopback HTTP/SSE contracts through the packaged executable. Provider-shaped overlap was 304/309/290/297 ms; every protocol passed image/header/credential/model/path checks and one-worker abort isolation. Scope was `127.0.0.1` only and no real Provider Account was contacted. +- Frozen install in an isolated WSL2 Ubuntu 24.04 x64 snapshot used Node `22.22.2` and the exact repository-pinned `pnpm@10.33.4`; native dependencies installed successfully. +- Linux workspace 5+5: cold ready p95 853 ms, warm ready p95 437 ms, RSS p95 170388 KiB, exit p95 8 ms. +- Linux staged 5+5: cold ready p95 428 ms, warm ready p95 440 ms, RSS p95 175956 KiB, exit p95 8 ms; all 131 Linux-applicable packages present and 8 platform-specific entries skipped. +- Linux controlled packaged 5+5: cold ready p95 545 ms, warm ready p95 569 ms, RSS p95 181928 KiB, exit p95 7 ms; all 131 applicable packages, six runtime assets, and unpacked native assets resolve from the artifact. +- Linux packaged loopback: all four contracts passed with 292/290/277/288 ms overlap, correct path/header/credential/model/image serialization, and isolated abort behavior. +- Post-fix `node scripts/probe-pi-packaged-runtime.mjs --samples 1 --timeout-ms 10000`: Windows controlled packaged smoke passed through the explicitly supplied resource directory; cold/warm ready were 650/641 ms and no dependency or asset was missing. +- Post-Provider-waiver `node scripts/probe-pi-runtime.mjs --samples 1 --timeout-ms 10000`: passed with cold/warm ready 650/588 ms. All four `providerMatrix.realTurnVerified` values remained `false`; real Provider evidence moved out of `missingEvidence` into explicit `waivers`, and macOS remained deferred to PI-150. The per-run `decision` remains `incomplete` because one local sample cannot issue the aggregate PI-000 decision. +- Post-probe process queries on Windows and Linux: zero controlled artifact processes and zero Pi RPC-looking processes remained. +- Execution-environment inventory found no repository CI definition, macOS runner/SSH host, GitLab runner CLI, or Provider credential environment variable. The internal GitLab remote alone cannot produce the deferred macOS evidence or optional real-Provider diagnostics; neither absence remains a PI-000 blocker after the explicit user decisions. +- The evidence gate table was reconciled with the accepted Spec's normative numbering: QG-002 RPC lifecycle, QG-003 dependency closure, QG-004 Provider Accounts, QG-005 real-provider concurrency, QG-006 metrics, and QG-007 stop-and-amend condition. +- The planner synchronized the accepted Spec and ticket graph: 134 unique requirements across 19 traceability groups, 18 tickets, 31 edges, and an acyclic topology remain intact; recomputation produced exactly `PI-010`, `PI-020`, and `PI-030` as the new Ready Frontier. +- Detailed evidence: `.project-docs/50-evidence/topics/20260822-pi-runtime-qualification-c52e8a14__pi-runtime-qualification.md`. + +## Follow-ups + +- Real Provider fixtures may still be run as optional diagnostics, but they are no longer PI-000 or PI-150 acceptance requirements. Never backfill QG-004/QG-005 as Pass without actual evidence, and do not place secrets in fixtures or task records. +- At PI-150, run macOS x64 and macOS arm64 workspace/staged/final packaged samples, packaged loopback, closure/native/resource checks, and metrics before claiming cross-platform release readiness. Use separate frozen installs and artifacts for each architecture. +- If Linux qualification must cover a native desktop distribution or release package rather than the accepted controlled `dir` boundary, repeat the Linux probe outside WSL2; the current sample is explicitly Ubuntu 24.04 userspace on a WSL2 kernel. +- Any later confirmed runtime, packaging, concurrency, isolation, or performance failure is new evidence and must return to architecture review; this waiver records accepted uncertainty, not permission to misreport a known failure. + +## Promotion Candidates + +- Target: the planner's Pi runtime cutover ticket graph / ready frontier. + Proposal: mark PI-000 Done with explicit waivers and advance the Ready Frontier to `PI-010`, `PI-020`, and `PI-030`; keep macOS explicitly deferred to PI-150 and QG-004/QG-005 explicitly waived rather than passed. + Evidence: task-scoped PI-000 qualification evidence record. + Future impact: downstream implementation proceeds while carrying explicit real-Provider and deferred macOS uncertainty; loopback and packaged evidence remain the proven boundary. + Semantic conflicts: none after the planner's user-authorized Spec/ticket amendment. + Human confirmation required: already supplied by the user's 2026-08-22 waiver decisions; a later confirmed failure requires a new architecture decision. diff --git a/.project-docs/50-evidence/topics/20260822-pi-runtime-qualification-c52e8a14__pi-runtime-qualification.md b/.project-docs/50-evidence/topics/20260822-pi-runtime-qualification-c52e8a14__pi-runtime-qualification.md new file mode 100644 index 0000000..40b467c --- /dev/null +++ b/.project-docs/50-evidence/topics/20260822-pi-runtime-qualification-c52e8a14__pi-runtime-qualification.md @@ -0,0 +1,232 @@ +# PI-000 Pi Runtime Qualification Evidence + +## Scope And Decision State + +- Task: `20260822-pi-runtime-qualification-c52e8a14` +- Candidate: `@earendil-works/pi-coding-agent@0.84.2` +- Current decision: **PI-000 Done with explicit waivers**. This is not an all-gates Pass. +- Ready frontier consequence: `PI-010`, `PI-020`, and `PI-030` are unlocked by the planner's audited 18-node/31-edge DAG. +- macOS Phase-0 decision: **Explicitly Waived / Deferred by user on 2026-08-22**. This is not a Pass. macOS x64/arm64 qualification moves to `PI-150` final cross-platform release validation. +- Real Provider decision: `QG-004` and `QG-005` are **Explicitly Waived / Accepted Risk by user on 2026-08-22**, not Pass. They no longer block PI-000 or PI-150. +- Accepted unverified risk includes real authentication, endpoint/proxy/rate-limit/provider-specific response variation; base URL, header, environment credential, model and image compatibility; real provider concurrency/serialization; and cross-worker abort/event/session/model/credential isolation. +- This record summarizes reproducible evidence. Raw generated JSON reports, temporary staged runtimes, and controlled directory artifacts are intentionally not committed. + +## Confirmed Upstream Identity + +- npm package version: `0.84.2` +- npm CLI entry: `dist/cli.js` +- declared Node engine: `>=22.19.0` +- upstream tag: `v0.84.2` +- upstream commit and npm `gitHead`: `914cf1472e715297caa30db4b9535d534a9eb718` +- package pin in Makelore: exact `0.84.2`, without caret or tilde +- project package manager used for the lockfile: the repository-pinned `pnpm@10.33.4` + +The selected Electron `43.4.0` executable exposes Node `24.18.1` when launched with `ELECTRON_RUN_AS_NODE=1`, which satisfies Pi's declared engine floor. + +## Windows x64 Qualification Sample + +Environment: + +- OS: Windows x64, release `10.0.26200` +- Runtime seam: Electron executable plus `ELECTRON_RUN_AS_NODE=1` +- Artifact: a temporary production-only staging directory installed from Pi's published `npm-shrinkwrap.json` +- Startup samples: 5 cold and 5 warm +- Cold definition: fresh Pi config, session, and project directories for each process +- Warm definition: shared primed Pi config directory with fresh session and project directories for each process + +Measured result: + +| Measurement | Samples | p50 | p95 | max | Budget | +|---|---:|---:|---:|---:|---:| +| Cold `get_state` ready | 5 | 654 ms | 681 ms | 681 ms | 3000 ms | +| Warm `get_state` ready | 5 | 661 ms | 674 ms | 674 ms | 1500 ms | +| Worker RSS | 10 | 122780 KiB | 125780 KiB | 125780 KiB | report-only | +| RPC process exit | 10 | 14 ms | 16 ms | 16 ms | deterministic | + +All measured processes exited by closing stdin. No SIGTERM, force kill, or retained child was needed in the successful sample. + +The deterministic provider-failure sample used a closed loopback endpoint, disabled automatic retry, and reached `prompt` accepted followed by `agent_settled` with an assistant `error` stop reason in 42 ms. This is below the 10-second deterministic-failure limit and does not claim provider success. + +## Controlled Electron Builder Artifact Sample + +PI-000 also built a Windows x64 Electron Builder `dir` artifact from the same temporary Pi production closure. This is a controlled packaged-boundary probe, not Makelore's final product artifact and not the permanent `PI-030`/`PI-150` bundler wiring. It deliberately does not inherit the product's OpenCode, Python, UV, signing, installer, or other `electron-builder.yml` hooks. + +The packaged boundary was: + +- executable: `MakelorePiProbe.exe` +- archive: `resources/app.asar` +- native extraction: `resources/app.asar.unpacked` +- Pi CLI: `resources/app.asar/dist/cli.js` +- process seam: packaged executable plus `ELECTRON_RUN_AS_NODE=1` + +Artifact resolution confirmed Pi `0.84.2`, its CLI, all 130 Windows-applicable production packages, all six runtime WASM/native assets, and native `.node` extraction. The nine non-Windows lock entries were skipped by their declared platform constraints. Electron Builder flattened `@types/retry@0.12.0` from `p-retry/node_modules/@types/retry` to the artifact root `node_modules/@types/retry`; the verifier checks the same version at the resolved artifact location instead of requiring npm's original physical layout. + +Measured result from 5 cold and 5 warm packaged samples: + +| Measurement | Samples | p50 | p95 | max | Budget | +|---|---:|---:|---:|---:|---:| +| Cold `get_state` ready | 5 | 668 ms | 682 ms | 682 ms | 3000 ms | +| Warm `get_state` ready | 5 | 660 ms | 663 ms | 663 ms | 1500 ms | +| Worker RSS | 10 | 138404 KiB | 141320 KiB | 141320 KiB | report-only | +| RPC process exit | 10 | 16 ms | 18 ms | 18 ms | deterministic | + +The packaged session, deterministic failure settlement, reopen, local shell overlap, one-worker shell abort isolation, and stdin-close exit checks also passed. The deterministic failure settled in 36 ms; the overlapping local shell sample completed in 2098 ms. A post-run process query found zero controlled artifact processes and zero Pi RPC-looking processes. + +## Linux x64 Qualification Sample + +The Linux run used an isolated repository snapshot under WSL2 Ubuntu 24.04 x64. The snapshot installed the frozen lockfile with Node `22.22.2` and the exact repository-pinned `pnpm@10.33.4`; Pi then ran through Electron `43.4.0` and its embedded Node `24.18.1`. This exercises the Linux executable, Linux native dependency selection, asar layout, and process behavior. It is not evidence for a native compositor, AppImage/deb installer, or a non-WSL Linux kernel. + +Workspace 5+5 result: + +| Measurement | Samples | p50 | p95 | max | Budget | +|---|---:|---:|---:|---:|---:| +| Cold `get_state` ready | 5 | 426 ms | 853 ms | 853 ms | 3000 ms | +| Warm `get_state` ready | 5 | 432 ms | 437 ms | 437 ms | 1500 ms | +| Worker RSS | 10 | 168228 KiB | 170388 KiB | 170388 KiB | report-only | +| RPC process exit | 10 | 7 ms | 8 ms | 8 ms | deterministic | + +Temporary production-closure 5+5 result: + +| Measurement | Samples | p50 | p95 | max | Budget | +|---|---:|---:|---:|---:|---:| +| Cold `get_state` ready | 5 | 426 ms | 428 ms | 428 ms | 3000 ms | +| Warm `get_state` ready | 5 | 423 ms | 440 ms | 440 ms | 1500 ms | +| Worker RSS | 10 | 170380 KiB | 175956 KiB | 175956 KiB | report-only | +| RPC process exit | 10 | 7 ms | 8 ms | 8 ms | deterministic | + +Controlled Electron Builder `dir`/`app.asar` 5+5 result: + +| Measurement | Samples | p50 | p95 | max | Budget | +|---|---:|---:|---:|---:|---:| +| Cold `get_state` ready | 5 | 526 ms | 545 ms | 545 ms | 3000 ms | +| Warm `get_state` ready | 5 | 545 ms | 569 ms | 569 ms | 1500 ms | +| Worker RSS | 10 | 181412 KiB | 181928 KiB | 181928 KiB | report-only | +| RPC process exit | 10 | 7 ms | 7 ms | 7 ms | deterministic | + +The Linux shrinkwrap has 131 applicable packages and 8 OS/CPU-skipped entries. The controlled artifact resolved all applicable packages, the Pi CLI, all six tracked runtime assets, and every native asset that must live under `app.asar.unpacked`; no package or asset was missing. Electron Builder applied the same-version `@types/retry@0.12.0` flattening seen on Windows. Session persistence/reopen, deterministic failure settlement, local overlap, shell abort isolation, and stdin-close cleanup passed in all three boundaries. A final process listing contained only WSL init and the listing command itself—no packaged Pi or RPC worker remained. + +## Runtime And Session Behaviors Confirmed On Windows + +- Strict LF-only JSONL framing, including payloads containing U+2028 and U+2029. +- `get_state` response and request id correlation. +- Persistent session file creation after a settled assistant turn. +- Reopen by session file with the same stable session id. +- Session history remained readable after reopen; the sample contained six entries. +- Direct RPC shell execution returned the expected output. +- Idle `abort` was accepted. +- Two workers had distinct session ids. +- Two direct shell operations overlapped; total elapsed time was 2112 ms for a 4-second left command and 2-second right command. +- `abort_bash` cancelled the left worker without cancelling or corrupting the right worker. +- Resource discovery was explicitly disabled for the qualification sample with `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, `--no-context-files`, and `--no-approve`; startup ran with `--offline` and `PI_TELEMETRY=0`. + +The two-worker result proves local process/session/shell isolation only. It is not evidence of two overlapping real-provider turns. + +## Production Closure Evidence + +Pi's published production shrinkwrap contains 139 dependency package entries: + +- 130 entries are applicable and present on Windows x64. +- 9 entries are correctly skipped by OS/CPU constraints. +- Required image-processing asset is present: `@silvia-odwyer/photon-node/photon_rs_bg.wasm`. +- Applicable clipboard native binary is present: `@mariozechner/clipboard-win32-x64-msvc`. +- Pi TUI Windows x64/arm64 prebuilds and its packaged Darwin prebuilds are present in the staged closure. + +Published-package caveat: the shrinkwrap is production-only, but the published package still lists ten development dependencies. npm 11 validates those development entries before applying `--omit=dev`, so an unmodified `npm ci --omit=dev` rejects the package as out of sync. The qualification staging copy removes `devDependencies` from only the temporary copied package root and then installs the unchanged published shrinkwrap. This must be considered when `PI-030` defines the permanent staging mechanism. + +## Provider Contract Matrix + +Static Pi contract coverage and a packaged-runtime loopback HTTP/SSE contract sample are confirmed for all current Makelore protocol identifiers: + +| Makelore protocol | Pi configuration | Loopback endpoint | Windows / Linux overlap | Real turn | +|---|---|---|---:|---| +| `openai-completions` | `api: openai-completions` | `/v1/chat/completions` | 304 / 292 ms | not run — waived/unverified | +| `openai-responses` | `api: openai-responses` | `/v1/responses` | 309 / 290 ms | not run — waived/unverified | +| `anthropic-messages` | `api: anthropic-messages` | `/v1/messages` | 290 / 277 ms | not run — waived/unverified | +| `openrouter` | `api: openai-completions` plus `thinkingFormat/sessionAffinityFormat: openrouter` | `/v1/chat/completions` | 297 / 288 ms | not run — waived/unverified | + +For each protocol, the packaged runtime sent four POST requests through `127.0.0.1`, used the selected model, included the custom header, injected the environment-only credential into the protocol-appropriate header (`Authorization` or `x-api-key`), and carried the PNG image in request history. Two independent workers settled successfully with `stop`, their provider-shaped turns overlapped, aborting the next turn in one worker produced `aborted`, and the unaffected worker still settled with `stop`. All workers exited by closing stdin. + +The Anthropic contract has a meaningful base URL difference: its SDK appends `/v1/messages`, so the configured base URL is the provider root. The OpenAI-style clients receive a base URL ending in `/v1` and append `chat/completions` or `responses`. + +This loopback result proves Pi's request serialization, SSE parsing, image/header/model/credential plumbing, worker overlap, and abort isolation without external cost. It does **not** qualify any real Provider Account, provider authentication, proxy behavior, rate limits, or provider-specific response variation. + +Pi's custom provider contract supports base URL, environment-resolved API key, custom headers, model id, and text/image input declaration. The probe accepts a credential-free fixture file shaped as follows; the secret remains only in the named environment variable: + +```json +{ + "id": "qualification-provider", + "apiProtocol": "openai-responses", + "baseUrl": "https://provider.example/v1", + "apiKeyEnv": "QUALIFICATION_PROVIDER_KEY", + "headers": { "X-Provider-Header": "literal-or-$ENV_REFERENCE" }, + "model": { + "id": "provider-model-id", + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 8192 + } +} +``` + +Run a real sample with: + +```text +pnpm run probe:pi-runtime -- --provider-fixture [--image ] +``` + +The real-provider path is implemented to require two successful overlapping turns with distinct sessions, then abort a new turn in one worker while the other worker settles successfully. It reports only provider/model ids, environment-variable name, and header names; it does not print credential values. + +No recognized provider credential environment variable was available in the implementation environment, so no real-provider request was sent and no provider cost was incurred. + +Reproduce the local contract sample against an existing controlled artifact with: + +```text +pnpm run probe:pi-provider-contracts -- --electron-executable --cli-path +``` + +## Deferred macOS Execution Handoff For PI-150 + +A pre-execution portability review found and fixed one supported-path defect: the artifact-side closure inspector derived `resources` from `dirname(process.execPath)`, which is correct on Windows/Linux but points to `Contents/MacOS/resources` on macOS. The probe now computes the platform resource directory once and passes it into the packaged process, preserving the actual macOS location `Contents/Resources/app.asar`. A focused path test and a real Windows controlled packaged regression pass verify the shared logic. The user explicitly deferred actual macOS execution from PI-000 to PI-150 on 2026-08-22, so no macOS Pass is claimed and the late-discovery risk remains. + +Use a separate checkout and frozen install for each target architecture. Before installing, `node -p "process.platform + ' ' + process.arch + ' ' + process.version"` must report `darwin x64` or `darwin arm64` for the sample being collected. Do not reuse one architecture's `node_modules` or Electron download for the other architecture. + +Run this sequence in each architecture-specific checkout: + +```text +pnpm install --frozen-lockfile +node scripts/probe-pi-runtime.mjs --samples 5 --timeout-ms 10000 --report release/pi-runtime-macos-workspace.json +pnpm run probe:pi-runtime -- --samples 5 --timeout-ms 10000 --report release/pi-runtime-macos-staged.json +pnpm run probe:pi-runtime:packaged -- --samples 5 --timeout-ms 10000 --report release/pi-runtime-macos-packaged.json +``` + +Read `artifact.executable` and `artifact.appAsar` from the packaged JSON report. The Pi CLI path is `/dist/cli.js`. Run the credential-free packaged four-protocol contract sample before replacing the controlled artifact: + +```text +pnpm run probe:pi-provider-contracts -- --electron-executable --cli-path /dist/cli.js --artifact-label controlled-electron-builder-dir-app-asar --report release/pi-provider-contract-macos.json +``` + +For an optional, non-gating real Provider diagnostic, create one credential-free fixture per protocol being sampled and keep each secret only in the environment variable named by that fixture's `apiKeyEnv`. Reuse the existing packaged artifact through the runtime override so the run exercises the packaged Electron seam without rebuilding it for every protocol: + +```text +node scripts/probe-pi-runtime.mjs --samples 5 --timeout-ms 10000 --electron-executable --cli-path /dist/cli.js --artifact-label controlled-electron-builder-dir-app-asar --provider-fixture --image --report release/pi-provider--macos.json +``` + +The user waived requiring these commands for `openai-completions`, `openai-responses`, `anthropic-messages`, and `openrouter`. If an optional diagnostic is run, preserve only redacted metrics and protocol/provider/model identifiers; do not commit fixtures containing secrets, generated reports, controlled artifacts, caches, or user data. A final process query must show no packaged probe or Pi RPC worker remains. + +## Gate Status + +| Gate | Status | Evidence or blocker | +|---|---|---| +| QG-001 exact identity and pin | Pass | exact npm/tag/commit/engine/CLI and lockfile pin | +| QG-002 packaged Electron Node RPC lifecycle | Pass | Phase-0 scope is Windows x64 + Linux x64 by explicit user decision; both controlled artifacts spawn, become ready, open session, accept prompt, abort, settle, reopen, and exit cleanly. macOS is deferred to PI-150, not passed. | +| QG-003 production dependency closure | Pass | Phase-0 controlled Windows/Linux artifacts resolve all 130/131 applicable packages and six runtime assets. Final product artifacts and deferred macOS closure belong to PI-150. | +| QG-004 Provider Account matrix | Explicitly Waived / Accepted Risk | four-protocol loopback request/SSE/image contracts pass; real accounts, credentials, endpoints, proxies, authentication, rate-limit and provider-specific behavior remain unverified by user decision on 2026-08-22 | +| QG-005 real-provider worker concurrency | Explicitly Waived / Accepted Risk | local shell and loopback protocol turns overlap with isolated abort/model/credential state; real provider overlap/serialization and abort/event/session/model/credential isolation remain unverified by user decision on 2026-08-22 | +| QG-006 performance and cleanup metrics | Pass | Phase-0 Windows/Linux cold/warm/RSS/exit sample counts and p50/p95/max are recorded and within budgets. macOS metrics are explicitly deferred to PI-150. | +| QG-007 stop-and-amend condition | Pending / not triggered | no confirmed packaging, Provider, or budget failure yet; any mandatory-gate failure stops migration and requires the specified utility-process SDK amendment | + +## Remaining And Optional Evidence + +1. Real Provider fixtures are optional diagnostics after the explicit waiver. Their absence does not block PI-000 or PI-150, but the current record must continue to report every real turn as unverified; a later confirmed failure is new architecture evidence, not something the waiver converts into a Pass. +2. At PI-150, run the deferred macOS x64/arm64 workspace, staged, final packaged, loopback, closure/native/resource, and metrics matrix. Missing either architecture blocks a cross-platform release-ready claim even though it no longer blocks PI-000. +3. If the accepted Linux definition requires a native desktop distribution or release package, repeat the Linux sample outside WSL2 at PI-150; the current Phase-0 evidence is deliberately labeled as Ubuntu 24.04 userspace on WSL2 with a controlled `dir` artifact. diff --git a/package.json b/package.json index c0f7377..823f107 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "build": "pnpm run build:vite && node scripts/bundle-opencode.mjs && node scripts/run-electron-builder.mjs", "build:vite": "node --max-old-space-size=6144 ./node_modules/vite/bin/vite.js build", "perf:budget": "node scripts/check-performance-budget.mjs", + "probe:pi-runtime": "node scripts/probe-pi-runtime.mjs --stage", + "probe:pi-runtime:packaged": "node scripts/probe-pi-packaged-runtime.mjs", + "probe:pi-provider-contracts": "node scripts/probe-pi-provider-contracts.mjs", "bundle:opencode": "node scripts/bundle-opencode.mjs", "lint": "eslint . --fix", "lint:check": "eslint .", @@ -84,6 +87,7 @@ "dependencies": { "@napi-rs/canvas": "0.1.100", "@monaco-editor/react": "^4.7.0", + "@earendil-works/pi-coding-agent": "0.84.2", "@playwright/mcp": "0.0.78", "adm-zip": "0.5.16", "diff": "^9.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a8d2d2..b121a1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@earendil-works/pi-coding-agent': + specifier: 0.84.2 + version: 0.84.2(ws@8.20.0)(zod@4.3.6) '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -293,7 +296,7 @@ importers: version: 0.14.6 vitest: specifier: ^4.0.18 - version: 4.1.1(@types/node@25.5.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)) zustand: specifier: ^5.0.11 version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -325,6 +328,15 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@asamuzakjp/css-color@5.0.1': resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -335,6 +347,103 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.34': + resolution: {integrity: sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.29': + resolution: {integrity: sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.52': + resolution: {integrity: sha512-vsPPM+nMbKJlUCFU+eoGZbdxdxDIAX9LbpjSXaR5Ufpmqgp8TdYQnoExhLu4T3umW/JIIPny1ydbhWidZZYokQ==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.10': + resolution: {integrity: sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -482,6 +591,36 @@ packages: resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==} engines: {node: '>=22.12.0'} + '@earendil-works/pi-agent-core@0.84.2': + resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.84.2': + resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-client@0.84.2': + resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.84.2': + resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-protocol@0.84.2': + resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.84.2': + resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-tui@0.84.2': + resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==} + engines: {node: '>=22.19.0'} + '@electron-internal/extract-zip@1.0.4': resolution: {integrity: sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==} engines: {node: '>=22.12.0'} @@ -757,6 +896,15 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@grammyjs/runner@2.0.3': resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==} engines: {node: '>=12.20.0 || >=14.13.1'} @@ -980,6 +1128,74 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -1142,6 +1358,10 @@ packages: '@opencode-ai/sdk@1.18.9': resolution: {integrity: sha512-oDJSmsmiGW+3lNLmZYj3EpUkpiT3ITZBKffH3mrmu2KMJXlkxQ/Nvv7jqPffSM7o8lCdBZS/aCE+2GkA3/92gQ==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1782,10 +2002,53 @@ packages: cpu: [x64] os: [win32] + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@snazzah/davey-android-arm-eabi@0.1.11': resolution: {integrity: sha512-T1RYbNYKN6tLOcGIDKJd8OI6FBSEemwL7DOYdTMmhqfhhMr3YVN8WOhfoxGg63OcnpTN2e2c5tdY2bAx25RmQQ==} engines: {node: '>= 10'} @@ -2003,6 +2266,9 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2341,6 +2607,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2355,6 +2624,9 @@ packages: bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2374,6 +2646,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2428,6 +2703,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -2577,6 +2856,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2646,6 +2929,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + diff@9.0.0: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} @@ -2699,6 +2986,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + effect@4.0.0-beta.83: resolution: {integrity: sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==} @@ -2927,6 +3217,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2969,6 +3263,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -3016,6 +3314,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3024,6 +3330,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3056,6 +3366,10 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -3072,6 +3386,14 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3087,6 +3409,10 @@ packages: resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} engines: {node: ^12.20.0 || >=14.13.1} + grok-mermaid@0.2.2: + resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==} + engines: {node: '>=18'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3142,10 +3468,17 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3307,6 +3640,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3328,9 +3665,16 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3360,6 +3704,12 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + katex@0.16.45: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true @@ -3462,6 +3812,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -3653,6 +4008,10 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -3760,6 +4119,11 @@ packages: node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3769,6 +4133,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -3894,6 +4262,17 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@6.40.0: + resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + opencode-ai@1.18.9: resolution: {integrity: sha512-tqvu/hJ26c2dBj/V/uTHaQI3bMSpLck0hIgGXO2z7b11s5mYfnaq+K1CBjsg8Pp6EirfzwUYGzi85K/SvOgkKg==} cpu: [arm64, x64] @@ -3990,6 +4369,10 @@ packages: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4002,6 +4385,9 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4021,6 +4407,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4372,6 +4762,10 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4429,6 +4823,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -4722,6 +5121,9 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -4751,6 +5153,9 @@ packages: resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -4777,6 +5182,10 @@ packages: resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==} engines: {node: '>=22.19.0'} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4979,6 +5388,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -5145,6 +5558,12 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@anthropic-ai/sdk@0.91.1(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + '@asamuzakjp/css-color@5.0.1': dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -5163,6 +5582,220 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.5 + '@aws-sdk/util-locate-window': 3.965.10 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.5 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/eventstream-handler-node': 3.972.34 + '@aws-sdk/middleware-eventstream': 3.972.29 + '@aws-sdk/middleware-websocket': 3.972.52 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.34': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.29': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.52': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.10': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5363,6 +5996,91 @@ snapshots: - opusscript - utf-8-validate + '@earendil-works/pi-agent-core@0.84.2(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@earendil-works/pi-ai': 0.84.2(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-telemetry': 0.84.2 + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.3.7 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.84.2(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@earendil-works/pi-telemetry': 0.84.2 + '@google/genai': 1.52.0 + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.40.0(ws@8.20.0)(zod@4.3.6) + partial-json: 0.1.7 + typebox: 1.3.7 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-client@0.84.2': + dependencies: + '@earendil-works/pi-protocol': 0.84.2 + + '@earendil-works/pi-coding-agent@0.84.2(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@earendil-works/pi-agent-core': 0.84.2(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-ai': 0.84.2(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-client': 0.84.2 + '@earendil-works/pi-protocol': 0.84.2 + '@earendil-works/pi-tui': 0.84.2 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + grok-mermaid: 0.2.2 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.3.7 + undici: 8.9.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-protocol@0.84.2': + dependencies: + typebox: 1.3.7 + + '@earendil-works/pi-telemetry@0.84.2': {} + + '@earendil-works/pi-tui@0.84.2': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + '@electron-internal/extract-zip@1.0.4': {} '@electron/asar@3.4.1': @@ -5615,6 +6333,17 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@google/genai@1.52.0': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@grammyjs/runner@2.0.3(grammy@1.42.0(encoding@0.1.13))': dependencies: abort-controller: 3.0.0 @@ -5802,6 +6531,50 @@ snapshots: transitivePeerDependencies: - supports-color + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -5925,6 +6698,8 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@opentelemetry/api@1.9.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -6488,8 +7263,63 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true + '@silvia-odwyer/photon-node@0.3.4': {} + '@sindresorhus/is@4.6.0': {} + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@snazzah/davey-android-arm-eabi@0.1.11': optional: true @@ -6714,6 +7544,8 @@ snapshots: dependencies: '@types/node': 25.6.0 + '@types/retry@0.12.0': {} + '@types/trusted-types@2.0.7': optional: true @@ -7097,6 +7929,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} bl@4.1.0: @@ -7110,6 +7944,8 @@ snapshots: bottleneck@2.19.5: {} + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -7135,6 +7971,8 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -7225,6 +8063,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -7363,6 +8203,8 @@ snapshots: csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0(@noble/hashes@2.0.1): dependencies: whatwg-mimetype: 5.0.0 @@ -7427,6 +8269,8 @@ snapshots: didyoumean@1.2.2: {} + diff@8.0.4: {} + diff@9.0.0: {} dir-compare@4.2.0: @@ -7491,6 +8335,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + effect@4.0.0-beta.83: dependencies: '@standard-schema/spec': 1.1.0 @@ -7806,6 +8654,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7847,6 +8700,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fraction.js@5.3.4: {} fs-extra@10.1.0: @@ -7898,10 +8755,28 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.1 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7947,6 +8822,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -7974,6 +8855,19 @@ snapshots: gopd: 1.2.0 optional: true + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} got@11.8.6: @@ -8002,6 +8896,8 @@ snapshots: - encoding - supports-color + grok-mermaid@0.2.2: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -8105,10 +9001,16 @@ snapshots: dependencies: hermes-estree: 0.25.1 + highlight.js@10.7.3: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.2.7 + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): dependencies: '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) @@ -8247,6 +9149,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -8282,8 +9186,17 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8309,6 +9222,17 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + katex@0.16.45: dependencies: commander: 8.3.0 @@ -8401,6 +9325,8 @@ snapshots: marked@14.0.0: {} + marked@18.0.5: {} + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -8805,6 +9731,10 @@ snapshots: dependencies: brace-expansion: 5.0.5 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.12 @@ -8918,12 +9848,20 @@ snapshots: dependencies: semver: 7.7.4 + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -8977,6 +9915,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@6.40.0(ws@8.20.0)(zod@4.3.6): + optionalDependencies: + ws: 8.20.0 + zod: 4.3.6 + opencode-ai@1.18.9: optionalDependencies: opencode-darwin-arm64: 1.18.9 @@ -9063,6 +10006,11 @@ snapshots: p-map@7.0.4: {} + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + package-json-from-dist@1.0.1: {} parse-entities@4.0.2: @@ -9083,6 +10031,8 @@ snapshots: dependencies: entities: 6.0.1 + partial-json@0.1.7: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -9096,6 +10046,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + pathe@2.0.3: {} pe-library@0.4.1: {} @@ -9437,6 +10392,8 @@ snapshots: retry@0.12.0: {} + retry@0.13.1: {} + reusify@1.1.0: {} rimraf@2.6.3: @@ -9513,6 +10470,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 @@ -9891,6 +10850,8 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 + ts-algebra@2.0.0: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -9917,6 +10878,8 @@ snapshots: dependencies: tagged-tag: 1.0.0 + typebox@1.3.7: {} + typescript@5.9.3: {} uint8array-extras@1.5.0: {} @@ -9931,6 +10894,8 @@ snapshots: undici@8.1.0: {} + undici@8.9.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -10068,7 +11033,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.1(@types/node@25.5.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.1 '@vitest/mocker': 4.1.1(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)) @@ -10091,6 +11056,7 @@ snapshots: vite: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 25.5.0 jsdom: 28.1.0(@noble/hashes@2.0.1) transitivePeerDependencies: @@ -10108,6 +11074,8 @@ snapshots: web-namespaces@2.0.1: {} + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@8.0.1: {} diff --git a/scripts/probe-pi-packaged-runtime.mjs b/scripts/probe-pi-packaged-runtime.mjs new file mode 100644 index 0000000..2697f2e --- /dev/null +++ b/scripts/probe-pi-packaged-runtime.mjs @@ -0,0 +1,322 @@ +import { spawn } from 'node:child_process'; +import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { arch, platform, tmpdir } from 'node:os'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { Arch, Platform, build } from 'electron-builder'; +import { + parseProbeArgs, + resolvePiRuntime, + runProbe, + stagePiRuntime, + validatePiIdentity, + validateStagedClosure, +} from './probe-pi-runtime.mjs'; + +const PRODUCT_NAME = 'MakelorePiProbe'; +const ARTIFACT_LABEL = 'controlled-electron-builder-dir-app-asar'; + +async function pathExists(path) { + try { + await stat(path); + return true; + } catch { + return false; + } +} + +function currentTarget() { + let platformTarget; + if (platform() === 'win32') platformTarget = Platform.WINDOWS; + else if (platform() === 'darwin') platformTarget = Platform.MAC; + else if (platform() === 'linux') platformTarget = Platform.LINUX; + else throw new Error(`Unsupported packaging platform: ${platform()}`); + const architecture = Arch[arch()]; + if (architecture == null) throw new Error(`Unsupported packaging architecture: ${arch()}`); + return { platformTarget, architecture }; +} + +async function findPackagedExecutable(root) { + const matches = []; + const visit = async (directory) => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) await visit(path); + else if ( + (platform() === 'win32' && entry.name === `${PRODUCT_NAME}.exe`) + || (platform() === 'darwin' + && entry.name === PRODUCT_NAME + && directory.endsWith(`${sep}Contents${sep}MacOS`)) + || (platform() === 'linux' && entry.name === PRODUCT_NAME) + ) matches.push(path); + } + }; + await visit(root); + if (matches.length !== 1) { + throw new Error(`Expected one packaged ${PRODUCT_NAME} executable, found ${matches.length}`); + } + return matches[0]; +} + +export function packagedResourcesDirectory(executable, currentPlatform = platform()) { + return currentPlatform === 'darwin' + ? resolve(executable, '..', '..', 'Resources') + : join(dirname(executable), 'resources'); +} + +function runCommand(executable, args, options = {}) { + return new Promise((resolvePromise, reject) => { + const child = spawn(executable, args, { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); + child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (code === 0) resolvePromise({ stdout, stderr }); + else reject(new Error( + `${executable} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}: ${stderr || stdout}`, + )); + }); + }); +} + +async function inspectPackagedClosure(executable, resourcesDirectory, assets) { + const script = String.raw` + const fs = require('node:fs'); + const path = require('node:path'); + const resources = process.env.PI_PROBE_RESOURCES_DIRECTORY; + if (!resources) throw new Error('PI_PROBE_RESOURCES_DIRECTORY is required'); + const root = path.join(resources, 'app.asar'); + const unpackedRoot = path.join(resources, 'app.asar.unpacked'); + const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const shrinkwrap = JSON.parse(fs.readFileSync(path.join(root, 'npm-shrinkwrap.json'), 'utf8')); + const assets = JSON.parse(process.env.PI_PROBE_ASSETS_JSON); + const libc = process.platform === 'linux' + ? (process.report?.getReport?.().header?.glibcVersionRuntime ? 'glibc' : 'musl') + : undefined; + const constraintMatches = (constraints, current) => { + if (!constraints || constraints.length === 0 || !current) return true; + const positive = constraints.filter((value) => !value.startsWith('!')); + const negative = constraints.filter((value) => value.startsWith('!')).map((value) => value.slice(1)); + return !negative.includes(current) && (positive.length === 0 || positive.includes(current)); + }; + const supportsCurrentPlatform = (entry) => constraintMatches(entry.os, process.platform) + && constraintMatches(entry.cpu, process.arch) + && constraintMatches(entry.libc, libc); + const missingPackages = []; + const relocatedPackages = []; + let expectedPackages = 0; + let platformSkippedPackages = 0; + for (const [packagePath, entry] of Object.entries(shrinkwrap.packages || {})) { + if (!packagePath || entry.dev) continue; + if (!supportsCurrentPlatform(entry)) { + platformSkippedPackages += 1; + continue; + } + expectedPackages += 1; + const exactPath = path.join(root, ...packagePath.split('/')); + if (fs.existsSync(exactPath)) continue; + const nestedMarker = '/node_modules/'; + const packageTail = packagePath.includes(nestedMarker) + ? packagePath.slice(packagePath.lastIndexOf(nestedMarker) + nestedMarker.length) + : packagePath.slice('node_modules/'.length); + const nameParts = packageTail.split('/'); + const packageName = nameParts[0].startsWith('@') ? nameParts.slice(0, 2).join('/') : nameParts[0]; + const flattenedPath = path.join(root, 'node_modules', ...packageName.split('/')); + const flattenedPackageJson = path.join(flattenedPath, 'package.json'); + if (fs.existsSync(flattenedPackageJson)) { + const flattenedVersion = JSON.parse(fs.readFileSync(flattenedPackageJson, 'utf8')).version; + if (flattenedVersion === entry.version) { + relocatedPackages.push({ from: packagePath, to: 'node_modules/' + packageName, version: entry.version }); + continue; + } + } + missingPackages.push(packagePath); + } + const missingAssets = assets.filter((asset) => !fs.existsSync(path.join(root, ...asset.split('/')))); + const missingUnpackedNativeAssets = assets + .filter((asset) => asset.endsWith('.node')) + .filter((asset) => !fs.existsSync(path.join(unpackedRoot, ...asset.split('/')))); + process.stdout.write(JSON.stringify({ + packageName: packageJson.name, + packageVersion: packageJson.version, + cliExists: fs.existsSync(path.join(root, 'dist', 'cli.js')), + lockedPackages: Math.max(0, Object.keys(shrinkwrap.packages || {}).length - 1), + expectedPackages, + platformSkippedPackages, + missingPackages, + relocatedPackages, + assetCount: assets.length, + missingAssets, + missingUnpackedNativeAssets, + })); + `; + const { stdout } = await runCommand(executable, ['-e', script], { + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: '1', + PI_PROBE_RESOURCES_DIRECTORY: resourcesDirectory, + PI_PROBE_ASSETS_JSON: JSON.stringify(assets), + }, + }); + const result = JSON.parse(stdout.trim()); + if ( + !result.cliExists + || result.missingPackages.length > 0 + || result.missingAssets.length > 0 + || result.missingUnpackedNativeAssets.length > 0 + ) { + throw new Error(`Packaged Pi closure is incomplete: ${JSON.stringify(result)}`); + } + return result; +} + +function assertControlledOutput(projectRoot, outputDirectory) { + const relativePath = relative(projectRoot, outputDirectory); + if (!relativePath || relativePath.startsWith('..') || resolve(projectRoot, relativePath) !== outputDirectory) { + throw new Error(`Refusing uncontrolled packaged-probe output: ${outputDirectory}`); + } + if (relativePath.split(sep).join('/') !== 'release/pi-runtime-probe') { + throw new Error(`Unexpected packaged-probe output directory: ${outputDirectory}`); + } +} + +async function buildControlledArtifact(projectRoot, appDirectory, outputDirectory) { + const electronPackage = JSON.parse(await readFile(join(projectRoot, 'node_modules', 'electron', 'package.json'), 'utf8')); + const { platformTarget, architecture } = currentTarget(); + const buildResources = join(appDirectory, 'build-resources'); + await mkdir(buildResources); + const builderArtifacts = await build({ + // Use the staged Pi root as the project root so this controlled probe does + // not inherit Makelore's production hooks or unrelated extraResources. + projectDir: appDirectory, + targets: platformTarget.createTarget('dir', architecture), + publish: 'never', + config: { + appId: 'app.niancode.desktop.pi-probe', + productName: PRODUCT_NAME, + electronVersion: electronPackage.version, + electronDist: join(projectRoot, 'node_modules', 'electron', 'dist'), + directories: { + output: outputDirectory, + buildResources, + }, + files: [ + 'dist/**/*', + 'package.json', + 'npm-shrinkwrap.json', + 'node_modules/**/*', + ], + asar: true, + asarUnpack: ['**/*.node'], + npmRebuild: false, + win: { + executableName: PRODUCT_NAME, + signAndEditExecutable: false, + verifyUpdateCodeSignature: false, + }, + mac: { + identity: null, + hardenedRuntime: false, + }, + linux: { + executableName: PRODUCT_NAME, + }, + }, + }); + return builderArtifacts; +} + +export async function runPackagedProbe(options, projectRoot = process.cwd()) { + const resolvedProjectRoot = resolve(projectRoot); + const outputDirectory = resolve(resolvedProjectRoot, 'release', 'pi-runtime-probe'); + assertControlledOutput(resolvedProjectRoot, outputDirectory); + const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-packaged-probe-')); + const appDirectory = join(scratchRoot, 'app'); + try { + const sourceRuntime = await resolvePiRuntime(resolvedProjectRoot); + const identity = await validatePiIdentity(sourceRuntime); + await stagePiRuntime(sourceRuntime, appDirectory); + const stagedClosure = await validateStagedClosure(appDirectory); + await rm(outputDirectory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); + await mkdir(dirname(outputDirectory), { recursive: true }); + const builderArtifacts = await buildControlledArtifact( + resolvedProjectRoot, + appDirectory, + outputDirectory, + ); + const executable = await findPackagedExecutable(outputDirectory); + const resourcesDirectory = packagedResourcesDirectory(executable); + const appAsar = join(resourcesDirectory, 'app.asar'); + const cliPath = join(appAsar, 'dist', 'cli.js'); + if (!await pathExists(appAsar)) throw new Error(`Packaged app.asar is missing: ${appAsar}`); + const packagedClosure = await inspectPackagedClosure( + executable, + resourcesDirectory, + stagedClosure.assets, + ); + const runtime = await runProbe({ + ...options, + reportPath: undefined, + stage: false, + keepStage: false, + electronExecutablePath: executable, + cliPath, + artifactLabel: ARTIFACT_LABEL, + }, resolvedProjectRoot); + const report = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + identity, + artifact: { + label: ARTIFACT_LABEL, + outputDirectory, + executable, + appAsar, + appAsarUnpacked: join(resourcesDirectory, 'app.asar.unpacked'), + builderArtifacts, + }, + stagedClosure, + packagedClosure, + runtime, + result: runtime.result, + decision: 'incomplete', + }; + if (options.reportPath) { + await mkdir(dirname(options.reportPath), { recursive: true }); + await writeFile(options.reportPath, `${JSON.stringify(report, null, 2)}\n`); + } + return report; + } finally { + await rm(scratchRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); + } +} + +async function main() { + const options = parseProbeArgs(process.argv.slice(2)); + if (options.help) { + process.stdout.write('Usage: node scripts/probe-pi-packaged-runtime.mjs [probe options]\n'); + return; + } + if (options.stage || options.keepStage || options.electronExecutablePath) { + throw new Error('Packaged probe owns staging and runtime paths; do not pass staging or runtime override flags'); + } + const report = await runPackagedProbe(options); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + if (report.result === 'fail') process.exitCode = 1; +} + +const isMain = process.argv[1] + && pathToFileURL(resolve(process.argv[1])).href === import.meta.url; +if (isMain) { + main().catch((error) => { + process.stderr.write(`${error.stack ?? error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/probe-pi-provider-contracts.mjs b/scripts/probe-pi-provider-contracts.mjs new file mode 100644 index 0000000..8c67826 --- /dev/null +++ b/scripts/probe-pi-provider-contracts.mjs @@ -0,0 +1,324 @@ +import { createServer } from 'node:http'; +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + MAKELore_PROVIDER_PROTOCOLS, + parseProbeArgs, + resolvePiRuntime, + runProviderQualification, + validatePiIdentity, +} from './probe-pi-runtime.mjs'; + +const LOCAL_API_KEY_ENV = 'PI_PROBE_LOCAL_CONTRACT_KEY'; +const LOCAL_API_KEY = 'pi-local-contract-only'; +const CUSTOM_HEADER = 'x-makelore-pi-probe'; +const IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Z4N8AAAAASUVORK5CYII='; + +function readRequestBody(request) { + return new Promise((resolvePromise, reject) => { + const chunks = []; + request.on('data', (chunk) => chunks.push(chunk)); + request.once('end', () => { + try { + resolvePromise(JSON.parse(Buffer.concat(chunks).toString('utf8'))); + } catch (error) { + reject(error); + } + }); + request.once('error', reject); + }); +} + +function writeSse(response, event) { + if (response.destroyed || response.writableEnded) return; + response.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`); +} + +function finishResponse(response, events) { + setTimeout(() => { + if (response.destroyed || response.writableEnded) return; + for (const event of events) writeSse(response, event); + response.end(); + }, 250); +} + +function respondOpenAiCompletions(response, body) { + const id = `chatcmpl-local-${Date.now()}`; + response.writeHead(200, { 'content-type': 'text/event-stream' }); + response.write(`data: ${JSON.stringify({ + id, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: body.model, + choices: [{ index: 0, delta: { role: 'assistant', content: 'PI_PROVIDER_OK' }, finish_reason: null }], + })}\n\n`); + setTimeout(() => { + if (response.destroyed || response.writableEnded) return; + response.write(`data: ${JSON.stringify({ + id, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: body.model, + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + })}\n\n`); + response.end('data: [DONE]\n\n'); + }, 250); +} + +function responseEnvelope(id, model, status, output = []) { + return { + id, + object: 'response', + created_at: Math.floor(Date.now() / 1000), + status, + model, + output, + error: null, + incomplete_details: null, + usage: status === 'completed' + ? { + input_tokens: 1, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: 1, + output_tokens_details: { reasoning_tokens: 0 }, + total_tokens: 2, + } + : null, + }; +} + +function respondOpenAiResponses(response, body) { + const id = `resp_local_${Date.now()}`; + const item = { + id: `msg_local_${Date.now()}`, + type: 'message', + status: 'completed', + role: 'assistant', + content: [{ type: 'output_text', text: 'PI_PROVIDER_OK', annotations: [] }], + }; + response.writeHead(200, { 'content-type': 'text/event-stream' }); + writeSse(response, { type: 'response.created', response: responseEnvelope(id, body.model, 'in_progress') }); + writeSse(response, { type: 'response.output_item.added', output_index: 0, item: { ...item, status: 'in_progress', content: [] } }); + writeSse(response, { + type: 'response.output_text.delta', + item_id: item.id, + output_index: 0, + content_index: 0, + delta: 'PI_PROVIDER_OK', + }); + finishResponse(response, [ + { type: 'response.output_item.done', output_index: 0, item }, + { type: 'response.completed', response: responseEnvelope(id, body.model, 'completed', [item]) }, + ]); +} + +function respondAnthropic(response, body) { + const id = `msg_local_${Date.now()}`; + response.writeHead(200, { 'content-type': 'text/event-stream' }); + writeSse(response, { + type: 'message_start', + message: { + id, + type: 'message', + role: 'assistant', + model: body.model, + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 0 }, + }, + }); + writeSse(response, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }); + writeSse(response, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'PI_PROVIDER_OK' } }); + finishResponse(response, [ + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'end_turn', stop_sequence: null }, + usage: { output_tokens: 1 }, + }, + { type: 'message_stop' }, + ]); +} + +function protocolFromPath(pathname) { + return MAKELore_PROVIDER_PROTOCOLS.find((protocol) => pathname.startsWith(`/${protocol}/`)); +} + +async function startContractServer(requests) { + const server = createServer(async (request, response) => { + response.once('error', () => undefined); + try { + const body = await readRequestBody(request); + const protocol = protocolFromPath(request.url ?? ''); + if (!protocol) { + response.writeHead(404).end(); + return; + } + requests.push({ + protocol, + method: request.method, + path: request.url, + headers: request.headers, + model: body.model, + hasImage: JSON.stringify(body).includes(IMAGE_BASE64), + }); + if (protocol === 'anthropic-messages') respondAnthropic(response, body); + else if (protocol === 'openai-responses') respondOpenAiResponses(response, body); + else respondOpenAiCompletions(response, body); + } catch (error) { + if (!response.headersSent) response.writeHead(400, { 'content-type': 'application/json' }); + if (!response.destroyed && !response.writableEnded) { + response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); + } + } + }); + await new Promise((resolvePromise, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolvePromise); + }); + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('Local provider server did not expose a TCP port'); + return { + port: address.port, + close: () => new Promise((resolvePromise, reject) => { + server.close((error) => (error ? reject(error) : resolvePromise())); + }), + }; +} + +function expectedPath(protocol) { + const endpoint = protocol === 'openai-responses' + ? 'responses' + : protocol === 'anthropic-messages' + ? 'messages' + : 'chat/completions'; + return `/${protocol}/v1/${endpoint}`; +} + +function validateCapturedRequests(protocol, modelId, requests) { + const matching = requests.filter((request) => request.protocol === protocol); + const authHeader = protocol === 'anthropic-messages' ? 'x-api-key' : 'authorization'; + const expectedAuth = protocol === 'anthropic-messages' ? LOCAL_API_KEY : `Bearer ${LOCAL_API_KEY}`; + const problems = []; + if (matching.length !== 4) problems.push(`expected 4 requests, got ${matching.length}`); + if (matching.some((request) => request.method !== 'POST')) problems.push('non-POST request'); + if (matching.some((request) => request.path !== expectedPath(protocol))) problems.push('unexpected endpoint path'); + if (matching.some((request) => request.headers[authHeader] !== expectedAuth)) problems.push('credential header mismatch'); + if (matching.some((request) => request.headers[CUSTOM_HEADER] !== protocol)) problems.push('custom header mismatch'); + if (matching.some((request) => request.model !== modelId)) problems.push('model mismatch'); + if (matching.filter((request) => request.hasImage).length < 2) problems.push('image payload missing from initial turns'); + if (problems.length > 0) throw new Error(`${protocol} local contract failed: ${problems.join('; ')}`); + return { + requestCount: matching.length, + method: 'POST', + path: expectedPath(protocol), + credentialHeader: authHeader, + customHeader: CUSTOM_HEADER, + modelId, + imageRequests: matching.filter((request) => request.hasImage).length, + }; +} + +export async function runLocalProviderContracts(options, projectRoot = process.cwd()) { + const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-provider-contracts-')); + const requests = []; + const previousApiKey = process.env[LOCAL_API_KEY_ENV]; + const server = await startContractServer(requests); + process.env[LOCAL_API_KEY_ENV] = LOCAL_API_KEY; + try { + const sourceRuntime = await resolvePiRuntime(projectRoot); + const identity = await validatePiIdentity(sourceRuntime); + const runtime = options.electronExecutablePath + ? { + ...sourceRuntime, + electronExecutable: options.electronExecutablePath, + cliPath: options.cliPath, + } + : sourceRuntime; + const imagePath = join(scratchRoot, 'pixel.png'); + await writeFile(imagePath, Buffer.from(IMAGE_BASE64, 'base64')); + const protocols = []; + for (const protocol of MAKELore_PROVIDER_PROTOCOLS) { + const protocolRoot = join(scratchRoot, protocol); + await mkdir(protocolRoot); + const fixturePath = join(protocolRoot, 'fixture.json'); + const modelId = `makelore-local-${protocol}`; + const baseUrl = protocol === 'anthropic-messages' + ? `http://127.0.0.1:${server.port}/${protocol}` + : `http://127.0.0.1:${server.port}/${protocol}/v1`; + await writeFile(fixturePath, `${JSON.stringify({ + id: `makelore-local-${protocol}`, + apiProtocol: protocol, + baseUrl, + apiKeyEnv: LOCAL_API_KEY_ENV, + headers: { [CUSTOM_HEADER]: protocol }, + model: { id: modelId, input: ['text', 'image'] }, + }, null, 2)}\n`); + const qualification = await runProviderQualification( + runtime, + protocolRoot, + fixturePath, + imagePath, + options.timeoutMs, + ); + protocols.push({ + protocol, + qualification, + requestContract: validateCapturedRequests(protocol, modelId, requests), + }); + } + const report = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + identity, + runtime: { + artifact: options.artifactLabel ?? 'workspace-install', + executable: runtime.electronExecutable, + cliPath: runtime.cliPath, + }, + scope: { + network: '127.0.0.1 only', + realProvider: false, + limitation: 'Validates Pi HTTP/SSE request contracts and worker behavior; does not qualify a real provider account.', + }, + protocols, + result: 'pass', + }; + if (options.reportPath) { + await mkdir(dirname(options.reportPath), { recursive: true }); + await writeFile(options.reportPath, `${JSON.stringify(report, null, 2)}\n`); + } + return report; + } finally { + if (previousApiKey === undefined) delete process.env[LOCAL_API_KEY_ENV]; + else process.env[LOCAL_API_KEY_ENV] = previousApiKey; + await server.close(); + await rm(scratchRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); + } +} + +async function main() { + const options = parseProbeArgs(process.argv.slice(2)); + if (options.help) { + process.stdout.write('Usage: node scripts/probe-pi-provider-contracts.mjs [probe options]\n'); + return; + } + if (options.stage || options.keepStage || options.providerFixturePath || options.imagePath) { + throw new Error('Local provider contract probe owns its fixtures and does not accept staging/provider fixture flags'); + } + const report = await runLocalProviderContracts(options); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} + +const isMain = process.argv[1] + && pathToFileURL(resolve(process.argv[1])).href === import.meta.url; +if (isMain) { + main().catch((error) => { + process.stderr.write(`${error.stack ?? error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/probe-pi-runtime.mjs b/scripts/probe-pi-runtime.mjs new file mode 100644 index 0000000..08b6da6 --- /dev/null +++ b/scripts/probe-pi-runtime.mjs @@ -0,0 +1,1102 @@ +import { spawn } from 'node:child_process'; +import { StringDecoder } from 'node:string_decoder'; +import { + cp, + mkdtemp, + mkdir, + readFile, + readdir, + rm, + stat, + writeFile, +} from 'node:fs/promises'; +import { realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { arch, platform, release, tmpdir } from 'node:os'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { performance } from 'node:perf_hooks'; + +export const PI_RUNTIME_IDENTITY = Object.freeze({ + packageName: '@earendil-works/pi-coding-agent', + version: '0.84.2', + tag: 'v0.84.2', + gitHead: '914cf1472e715297caa30db4b9535d534a9eb718', + nodeEngine: '>=22.19.0', +}); + +export const MAKELore_PROVIDER_PROTOCOLS = Object.freeze([ + 'openai-completions', + 'openai-responses', + 'anthropic-messages', + 'openrouter', +]); + +export function buildMissingEvidence() { + return [ + 'Phase-0 artifact aggregation: Windows x64 and Linux x64; macOS x64/arm64 deferred to PI-150 by user decision on 2026-08-22', + ]; +} + +export function buildEvidenceWaivers() { + return [ + 'macOS x64/arm64 Phase-0 execution deferred to PI-150 by user decision on 2026-08-22; no macOS Pass is claimed', + 'QG-004/QG-005 real Provider Account compatibility and real-provider concurrency/isolation explicitly waived by user decision on 2026-08-22; real authentication, endpoint/proxy/rate-limit/provider variation, protocol compatibility, concurrency, abort/event/session/model/credential isolation, and image behavior remain unverified', + ]; +} + +const DEFAULT_TIMEOUT_MS = 10_000; +const DEFAULT_SAMPLE_COUNT = 5; +const MAX_STDERR_LENGTH = 16_000; + +function parsePositiveInteger(value, flag) { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${flag} must be a positive integer`); + } + return parsed; +} + +export function parseProbeArgs(argv) { + const options = { + samples: DEFAULT_SAMPLE_COUNT, + timeoutMs: DEFAULT_TIMEOUT_MS, + stage: false, + keepStage: false, + reportPath: undefined, + providerFixturePath: undefined, + imagePath: undefined, + electronExecutablePath: undefined, + cliPath: undefined, + artifactLabel: undefined, + }; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + const next = () => { + const value = argv[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`${argument} requires a value`); + } + index += 1; + return value; + }; + + if (argument === '--samples') options.samples = parsePositiveInteger(next(), argument); + else if (argument === '--timeout-ms') options.timeoutMs = parsePositiveInteger(next(), argument); + else if (argument === '--stage') options.stage = true; + else if (argument === '--keep-stage') options.keepStage = true; + else if (argument === '--report') options.reportPath = resolve(next()); + else if (argument === '--provider-fixture') options.providerFixturePath = resolve(next()); + else if (argument === '--image') options.imagePath = resolve(next()); + else if (argument === '--electron-executable') options.electronExecutablePath = resolve(next()); + else if (argument === '--cli-path') options.cliPath = resolve(next()); + else if (argument === '--artifact-label') options.artifactLabel = next(); + else if (argument === '--help') options.help = true; + else throw new Error(`Unknown argument: ${argument}`); + } + + if (options.keepStage && !options.stage) { + throw new Error('--keep-stage requires --stage'); + } + if (options.imagePath && !options.providerFixturePath) { + throw new Error('--image requires --provider-fixture'); + } + if (Boolean(options.electronExecutablePath) !== Boolean(options.cliPath)) { + throw new Error('--electron-executable and --cli-path must be provided together'); + } + if (options.stage && options.electronExecutablePath) { + throw new Error('--stage cannot be combined with a packaged runtime override'); + } + return options; +} + +export function createStrictJsonlParser(onRecord) { + const decoder = new StringDecoder('utf8'); + let buffer = ''; + + const emitLines = () => { + while (true) { + const newlineIndex = buffer.indexOf('\n'); + if (newlineIndex === -1) return; + let line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + if (line.endsWith('\r')) line = line.slice(0, -1); + if (line.length > 0) onRecord(JSON.parse(line)); + } + }; + + return { + push(chunk) { + buffer += typeof chunk === 'string' ? chunk : decoder.write(chunk); + emitLines(); + }, + finish() { + buffer += decoder.end(); + if (buffer.length > 0) { + const line = buffer.endsWith('\r') ? buffer.slice(0, -1) : buffer; + buffer = ''; + if (line.length > 0) onRecord(JSON.parse(line)); + } + }, + }; +} + +export function percentile(values, requestedPercentile) { + if (values.length === 0) return null; + const sorted = [...values].sort((left, right) => left - right); + const rank = Math.ceil((requestedPercentile / 100) * sorted.length) - 1; + return sorted[Math.max(0, Math.min(sorted.length - 1, rank))]; +} + +export function summarizeMeasurements(values) { + if (values.length === 0) { + return { samples: 0, p50: null, p95: null, max: null }; + } + return { + samples: values.length, + p50: Math.round(percentile(values, 50)), + p95: Math.round(percentile(values, 95)), + max: Math.round(Math.max(...values)), + }; +} + +function readJson(path) { + return readFile(path, 'utf8').then((source) => JSON.parse(source)); +} + +async function pathExists(path) { + try { + await stat(path); + return true; + } catch { + return false; + } +} + +export async function resolvePiRuntime(projectRoot = process.cwd()) { + const resolvedProjectRoot = resolve(projectRoot); + const requireFromProject = createRequire(join(resolvedProjectRoot, 'package.json')); + const electronExecutable = requireFromProject('electron'); + const packageRoot = realpathSync(join( + resolvedProjectRoot, + 'node_modules', + ...PI_RUNTIME_IDENTITY.packageName.split('/'), + )); + return { + projectRoot: resolvedProjectRoot, + electronExecutable, + packageRoot, + cliPath: join(packageRoot, 'dist', 'cli.js'), + }; +} + +export async function validatePiIdentity(runtime) { + const rootPackage = await readJson(join(runtime.projectRoot, 'package.json')); + const piPackage = await readJson(join(runtime.packageRoot, 'package.json')); + const pinnedVersion = rootPackage.dependencies?.[PI_RUNTIME_IDENTITY.packageName]; + const problems = []; + + if (pinnedVersion !== PI_RUNTIME_IDENTITY.version) { + problems.push(`root dependency must be exactly ${PI_RUNTIME_IDENTITY.version}, got ${pinnedVersion ?? 'missing'}`); + } + if (piPackage.version !== PI_RUNTIME_IDENTITY.version) { + problems.push(`installed package must be ${PI_RUNTIME_IDENTITY.version}, got ${piPackage.version ?? 'missing'}`); + } + if (piPackage.engines?.node !== PI_RUNTIME_IDENTITY.nodeEngine) { + problems.push(`Pi Node engine must be ${PI_RUNTIME_IDENTITY.nodeEngine}, got ${piPackage.engines?.node ?? 'missing'}`); + } + if (piPackage.bin?.pi !== 'dist/cli.js') { + problems.push(`Pi CLI entry must be dist/cli.js, got ${piPackage.bin?.pi ?? 'missing'}`); + } + if (!await pathExists(runtime.cliPath)) problems.push(`Pi CLI does not exist at ${runtime.cliPath}`); + if (!await pathExists(runtime.electronExecutable)) { + problems.push(`Electron executable does not exist at ${runtime.electronExecutable}`); + } + if (problems.length > 0) throw new Error(problems.join('; ')); + + return { + ...PI_RUNTIME_IDENTITY, + installedPackageRoot: runtime.packageRoot, + cliEntry: relative(runtime.packageRoot, runtime.cliPath), + }; +} + +function runCommand(executable, args, options = {}) { + return new Promise((resolvePromise, reject) => { + const child = spawn(executable, args, { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); + child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (code === 0) resolvePromise({ stdout, stderr }); + else reject(new Error(`${executable} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}: ${stderr || stdout}`)); + }); + }); +} + +async function inspectElectronNodeRuntime(executable) { + const { stdout } = await runCommand( + executable, + ['-p', 'JSON.stringify(process.versions)'], + { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } }, + ); + const versions = JSON.parse(stdout.trim()); + const [major, minor] = versions.node.split('.').map((value) => Number.parseInt(value, 10)); + if (major < 22 || (major === 22 && minor < 19)) { + throw new Error(`Electron Node ${versions.node} does not satisfy Pi ${PI_RUNTIME_IDENTITY.nodeEngine}`); + } + return { + electron: versions.electron, + node: versions.node, + modules: versions.modules, + napi: versions.napi, + }; +} + +export async function stagePiRuntime(runtime, destination) { + await mkdir(destination, { recursive: true }); + await cp(runtime.packageRoot, destination, { + recursive: true, + filter(source) { + return relative(runtime.packageRoot, source).split(sep)[0] !== 'node_modules'; + }, + }); + + // The published shrinkwrap intentionally contains the production graph only, + // while the published package.json still lists development dependencies. npm + // validates those dev entries before applying --omit=dev, so make the copied + // qualification root match the production-only shrinkwrap before npm ci. + const stagedPackagePath = join(destination, 'package.json'); + const stagedPackage = await readJson(stagedPackagePath); + const omittedDevDependencies = Object.keys(stagedPackage.devDependencies ?? {}).length; + delete stagedPackage.devDependencies; + await writeFile(stagedPackagePath, `${JSON.stringify(stagedPackage, null, 2)}\n`); + + const npmCli = join( + realpathSync(join(runtime.projectRoot, 'node_modules', 'npm')), + 'bin', + 'npm-cli.js', + ); + const install = await runCommand(process.execPath, [ + npmCli, + 'ci', + '--omit=dev', + '--ignore-scripts', + '--no-audit', + '--no-fund', + ], { + cwd: destination, + env: { + ...process.env, + npm_config_update_notifier: 'false', + }, + }); + return { destination, installStderr: install.stderr.trim(), omittedDevDependencies }; +} + +function currentLibc() { + if (platform() !== 'linux') return undefined; + return process.report?.getReport?.().header?.glibcVersionRuntime ? 'glibc' : 'musl'; +} + +function constraintMatches(constraints, current) { + if (!constraints || constraints.length === 0 || !current) return true; + const positive = constraints.filter((value) => !value.startsWith('!')); + const negative = constraints.filter((value) => value.startsWith('!')).map((value) => value.slice(1)); + return !negative.includes(current) && (positive.length === 0 || positive.includes(current)); +} + +export function lockEntrySupportsCurrentPlatform(entry) { + return constraintMatches(entry.os, platform()) + && constraintMatches(entry.cpu, arch()) + && constraintMatches(entry.libc, currentLibc()); +} + +async function collectRuntimeAssets(root) { + const assets = []; + const visit = async (directory) => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) await visit(path); + else if (entry.name.endsWith('.wasm') || entry.name.endsWith('.node')) { + assets.push(relative(root, path).split(sep).join('/')); + } + } + }; + for (const runtimeDirectory of ['dist', 'node_modules']) { + const path = join(root, runtimeDirectory); + if (await pathExists(path)) await visit(path); + } + return assets.sort(); +} + +export async function validateStagedClosure(stageRoot) { + const shrinkwrap = await readJson(join(stageRoot, 'npm-shrinkwrap.json')); + const missing = []; + let expectedPackages = 0; + let platformSkippedPackages = 0; + + for (const [packagePath, entry] of Object.entries(shrinkwrap.packages ?? {})) { + if (!packagePath || entry.dev) continue; + if (!lockEntrySupportsCurrentPlatform(entry)) { + platformSkippedPackages += 1; + continue; + } + expectedPackages += 1; + if (!await pathExists(join(stageRoot, ...packagePath.split('/')))) missing.push(packagePath); + } + + const requiredWasm = 'node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm'; + if (!await pathExists(join(stageRoot, ...requiredWasm.split('/')))) missing.push(requiredWasm); + if (missing.length > 0) { + throw new Error(`Staged Pi production closure is incomplete: ${missing.join(', ')}`); + } + + return { + lockfileVersion: shrinkwrap.lockfileVersion, + lockedPackages: Math.max(0, Object.keys(shrinkwrap.packages ?? {}).length - 1), + expectedPackages, + platformSkippedPackages, + assets: await collectRuntimeAssets(stageRoot), + }; +} + +function timeoutAfter(milliseconds, message) { + let timer; + const promise = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), milliseconds); + }); + return { promise, cancel: () => clearTimeout(timer) }; +} + +export class PiRpcWorker { + constructor({ executable, cliPath, cwd, env, args, timeoutMs = DEFAULT_TIMEOUT_MS }) { + this.executable = executable; + this.cliPath = cliPath; + this.cwd = cwd; + this.env = env; + this.args = args; + this.timeoutMs = timeoutMs; + this.pending = new Map(); + this.waiters = []; + this.events = []; + this.stderr = ''; + this.sequence = 0; + } + + async start() { + if (this.child) throw new Error('Pi RPC worker already started'); + this.startedAt = performance.now(); + this.child = spawn(this.executable, [this.cliPath, ...this.args], { + cwd: this.cwd, + env: { ...process.env, ...this.env, ELECTRON_RUN_AS_NODE: '1' }, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }); + this.exitResult = new Promise((resolveExit, rejectExit) => { + this.child.once('error', rejectExit); + this.child.once('exit', (code, signal) => { + this.exitedAt = performance.now(); + const result = { code, signal }; + resolveExit(result); + for (const pending of this.pending.values()) { + pending.reject(new Error(`Pi RPC worker exited before response (code=${code}, signal=${signal})`)); + } + this.pending.clear(); + }); + }); + + const parser = createStrictJsonlParser((record) => this.handleRecord(record)); + this.child.stdout.on('data', (chunk) => parser.push(chunk)); + this.child.stdout.on('end', () => parser.finish()); + this.child.stderr.on('data', (chunk) => { + this.stderr = `${this.stderr}${chunk.toString()}`.slice(-MAX_STDERR_LENGTH); + }); + + await new Promise((resolveSpawn, rejectSpawn) => { + this.child.once('spawn', resolveSpawn); + this.child.once('error', rejectSpawn); + }); + return this; + } + + handleRecord(record) { + record.receivedAt = performance.now(); + if (record.type === 'response' && record.id && this.pending.has(record.id)) { + const pending = this.pending.get(record.id); + this.pending.delete(record.id); + pending.resolve(record); + return; + } + this.events.push(record); + for (const waiter of [...this.waiters]) { + if (!waiter.predicate(record)) continue; + this.waiters.splice(this.waiters.indexOf(waiter), 1); + waiter.cancelTimeout(); + waiter.resolve(record); + } + } + + async request(command, timeoutMs = this.timeoutMs) { + const id = `probe-${++this.sequence}`; + const timeout = timeoutAfter(timeoutMs, `Pi RPC command ${command.type} timed out after ${timeoutMs}ms`); + const response = new Promise((resolveResponse, reject) => { + this.pending.set(id, { resolve: resolveResponse, reject }); + }); + this.child.stdin.write(`${JSON.stringify({ ...command, id })}\n`); + try { + const record = await Promise.race([response, timeout.promise]); + if (!record.success) throw new Error(`Pi RPC ${record.command} failed: ${record.error}`); + return record; + } finally { + timeout.cancel(); + this.pending.delete(id); + } + } + + waitForEvent(predicate, timeoutMs = this.timeoutMs) { + const existing = this.events.find(predicate); + if (existing) return Promise.resolve(existing); + return new Promise((resolveEvent, reject) => { + const timeout = timeoutAfter(timeoutMs, `Pi RPC event timed out after ${timeoutMs}ms`); + const waiter = { + predicate, + resolve: resolveEvent, + cancelTimeout: timeout.cancel, + }; + this.waiters.push(waiter); + timeout.promise.catch((error) => { + const index = this.waiters.indexOf(waiter); + if (index !== -1) this.waiters.splice(index, 1); + reject(error); + }); + }); + } + + stop(graceMs = 2_000) { + if (!this.stopPromise) this.stopPromise = this.performStop(graceMs); + return this.stopPromise; + } + + async performStop(graceMs) { + if (!this.child) return { mode: 'not-started', code: null, signal: null, exitMs: 0 }; + const stopStartedAt = performance.now(); + this.child.stdin.end(); + const gracefulTimeout = timeoutAfter(graceMs, 'Pi RPC stdin close did not exit'); + try { + const result = await Promise.race([this.exitResult, gracefulTimeout.promise]); + return { mode: 'stdin-close', ...result, exitMs: Math.round(performance.now() - stopStartedAt) }; + } catch { + this.child.kill('SIGTERM'); + const terminateTimeout = timeoutAfter(graceMs, 'Pi RPC SIGTERM did not exit'); + try { + const result = await Promise.race([this.exitResult, terminateTimeout.promise]); + return { mode: 'sigterm', ...result, exitMs: Math.round(performance.now() - stopStartedAt) }; + } catch { + if (platform() === 'win32') { + await runCommand('taskkill.exe', ['/pid', String(this.child.pid), '/t', '/f']).catch(() => undefined); + } else { + this.child.kill('SIGKILL'); + } + const result = await this.exitResult; + return { + mode: platform() === 'win32' ? 'taskkill' : 'sigkill', + ...result, + exitMs: Math.round(performance.now() - stopStartedAt), + }; + } finally { + terminateTimeout.cancel(); + } + } finally { + gracefulTimeout.cancel(); + } + } +} + +async function getProcessRssKb(pid) { + if (!pid) return null; + try { + if (platform() === 'win32') { + const { stdout } = await runCommand('tasklist.exe', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh']); + const columns = stdout.trim().replace(/^"|"$/g, '').split('","'); + const digits = columns.at(-1)?.replace(/[^0-9]/g, ''); + return digits ? Number.parseInt(digits, 10) : null; + } + const { stdout } = await runCommand('ps', ['-o', 'rss=', '-p', String(pid)]); + return Number.parseInt(stdout.trim(), 10) || null; + } catch { + return null; + } +} + +function baseRpcArgs(sessionDir, extraArgs = []) { + return [ + '--mode', 'rpc', + '--offline', + '--session-dir', sessionDir, + '--no-extensions', + '--no-skills', + '--no-prompt-templates', + '--no-themes', + '--no-context-files', + '--no-approve', + '--no-tools', + ...extraArgs, + ]; +} + +async function makeWorkerPaths(root, prefix) { + const base = await mkdtemp(join(root, `${prefix}-`)); + const configDir = join(base, 'config'); + const sessionDir = join(base, 'sessions'); + const cwd = join(base, 'project'); + await Promise.all([mkdir(configDir), mkdir(sessionDir), mkdir(cwd)]); + return { base, configDir, sessionDir, cwd }; +} + +function workerEnv(configDir) { + return { + PI_CODING_AGENT_DIR: configDir, + PI_OFFLINE: '1', + PI_TELEMETRY: '0', + }; +} + +async function runReadySample(runtime, paths, timeoutMs) { + const worker = new PiRpcWorker({ + executable: runtime.electronExecutable, + cliPath: runtime.cliPath, + cwd: paths.cwd, + env: workerEnv(paths.configDir), + args: baseRpcArgs(paths.sessionDir, ['--session-id', `probe-${Date.now()}-${Math.random()}`]), + timeoutMs, + }); + try { + await worker.start(); + const response = await worker.request({ type: 'get_state' }); + const readyMs = response.receivedAt - worker.startedAt; + const rssKb = await getProcessRssKb(worker.child.pid); + const stop = await worker.stop(); + return { readyMs, rssKb, stop, sessionId: response.data.sessionId }; + } finally { + await worker.stop().catch(() => undefined); + } +} + +async function runPerformanceSamples(runtime, scratchRoot, sampleCount, timeoutMs) { + const cold = []; + for (let index = 0; index < sampleCount; index += 1) { + const paths = await makeWorkerPaths(scratchRoot, `cold-${index}`); + cold.push(await runReadySample(runtime, paths, timeoutMs)); + } + + const warmConfig = join(scratchRoot, 'warm-config'); + await mkdir(warmConfig); + const warm = []; + for (let index = 0; index < sampleCount; index += 1) { + const paths = await makeWorkerPaths(scratchRoot, `warm-${index}`); + paths.configDir = warmConfig; + warm.push(await runReadySample(runtime, paths, timeoutMs)); + } + + return { + definition: { + cold: 'fresh Pi config, session, and project directories per process', + warm: 'shared primed Pi config directory with fresh session and project directories per process', + }, + coldReadyMs: summarizeMeasurements(cold.map((sample) => sample.readyMs)), + warmReadyMs: summarizeMeasurements(warm.map((sample) => sample.readyMs)), + rssKb: summarizeMeasurements([...cold, ...warm].flatMap((sample) => sample.rssKb == null ? [] : [sample.rssKb])), + exitMs: summarizeMeasurements([...cold, ...warm].map((sample) => sample.stop.exitMs)), + exitModes: [...new Set([...cold, ...warm].map((sample) => sample.stop.mode))], + }; +} + +async function runSessionLifecycle(runtime, scratchRoot, timeoutMs) { + const paths = await makeWorkerPaths(scratchRoot, 'session'); + const failureFixture = { + id: 'makelore-deterministic-failure', + apiProtocol: 'openai-completions', + baseUrl: 'http://127.0.0.1:1/v1', + apiKeyEnv: 'PI_PROBE_FAILURE_KEY', + model: { id: 'pi-probe-failure-model' }, + }; + await writeFile( + join(paths.configDir, 'models.json'), + `${JSON.stringify(buildPiProviderConfig(failureFixture), null, 2)}\n`, + ); + const common = { + executable: runtime.electronExecutable, + cliPath: runtime.cliPath, + cwd: paths.cwd, + env: { ...workerEnv(paths.configDir), PI_PROBE_FAILURE_KEY: 'qualification-only' }, + timeoutMs, + }; + const first = new PiRpcWorker({ + ...common, + args: baseRpcArgs(paths.sessionDir, [ + '--session-id', `probe-session-${Date.now()}`, + '--provider', failureFixture.id, + '--model', failureFixture.model.id, + ]), + }); + let reopened; + try { + await first.start(); + const initial = await first.request({ type: 'get_state' }); + await first.request({ type: 'set_session_name', name: 'PI runtime qualification' }); + const shell = await first.request({ type: 'bash', command: 'echo PI_RPC_SHELL_OK' }); + await first.request({ type: 'abort' }); + await first.request({ type: 'set_auto_retry', enabled: false }); + const failureStartedAt = performance.now(); + const failedTurn = await promptAndSettle( + first, + 'This request must fail against the closed local probe endpoint.', + undefined, + timeoutMs, + ); + const deterministicFailureMs = performance.now() - failureStartedAt; + const firstStop = await first.stop(); + if (!initial.data.sessionFile || !await pathExists(initial.data.sessionFile)) { + throw new Error('Pi RPC session file was not persisted'); + } + + reopened = new PiRpcWorker({ + ...common, + args: baseRpcArgs(paths.sessionDir, [ + '--session', initial.data.sessionFile, + '--provider', failureFixture.id, + '--model', failureFixture.model.id, + ]), + }); + await reopened.start(); + const reopenedState = await reopened.request({ type: 'get_state' }); + const entries = await reopened.request({ type: 'get_entries' }); + const reopenedStop = await reopened.stop(); + if (reopenedState.data.sessionId !== initial.data.sessionId) { + throw new Error('Pi RPC reopened a different session id'); + } + + return { + sessionIdPreserved: true, + sessionFilePersisted: true, + entryCount: entries.data.entries.length, + shellOutputMatched: shell.data.output.includes('PI_RPC_SHELL_OK'), + idleAbortAccepted: true, + promptAccepted: true, + agentSettled: true, + deterministicFailureStopReason: failedTurn.stopReason, + deterministicFailureMs: Math.round(deterministicFailureMs), + exitModes: [firstStop.mode, reopenedStop.mode], + }; + } finally { + await Promise.all([ + first.stop().catch(() => undefined), + reopened?.stop().catch(() => undefined), + ]); + } +} + +function longShellCommand(label, seconds) { + if (platform() === 'win32') return `ping 127.0.0.1 -n ${seconds + 1} > nul && echo ${label}`; + return `sleep ${seconds}; echo ${label}`; +} + +async function runLocalWorkerIsolation(runtime, scratchRoot, timeoutMs) { + const [leftPaths, rightPaths] = await Promise.all([ + makeWorkerPaths(scratchRoot, 'worker-left'), + makeWorkerPaths(scratchRoot, 'worker-right'), + ]); + const create = (paths) => new PiRpcWorker({ + executable: runtime.electronExecutable, + cliPath: runtime.cliPath, + cwd: paths.cwd, + env: workerEnv(paths.configDir), + args: baseRpcArgs(paths.sessionDir), + timeoutMs, + }); + const left = create(leftPaths); + const right = create(rightPaths); + try { + await Promise.all([left.start(), right.start()]); + const [leftState, rightState] = await Promise.all([ + left.request({ type: 'get_state' }), + right.request({ type: 'get_state' }), + ]); + + const startedAt = performance.now(); + const leftBash = left.request({ type: 'bash', command: longShellCommand('PI_LEFT_ABORTED', 4) }); + const rightBash = right.request({ type: 'bash', command: longShellCommand('PI_RIGHT_OK', 2) }); + await new Promise((resolveWait) => setTimeout(resolveWait, 500)); + await left.request({ type: 'abort_bash' }); + const [leftResult, rightResult] = await Promise.all([leftBash, rightBash]); + const elapsedMs = performance.now() - startedAt; + const stops = await Promise.all([left.stop(), right.stop()]); + + if (leftState.data.sessionId === rightState.data.sessionId) { + throw new Error('Independent Pi workers shared a session id'); + } + if (!leftResult.data.cancelled) throw new Error('Aborted Pi bash command was not marked cancelled'); + if (rightResult.data.cancelled || !rightResult.data.output.includes('PI_RIGHT_OK')) { + throw new Error('Aborting one Pi worker affected the other worker'); + } + + return { + distinctSessionIds: true, + abortIsolated: true, + overlappingLocalWork: elapsedMs < 3_500, + elapsedMs: Math.round(elapsedMs), + exitModes: stops.map((stop) => stop.mode), + limitation: 'This proves local worker and shell isolation, not overlapping real-provider turns.', + }; + } finally { + await Promise.all([ + left.stop().catch(() => undefined), + right.stop().catch(() => undefined), + ]); + } +} + +export function mapMakeloreProtocolToPi(protocol) { + if (!MAKELore_PROVIDER_PROTOCOLS.includes(protocol)) { + throw new Error(`Unsupported Makelore provider protocol: ${protocol}`); + } + if (protocol === 'openrouter') { + return { + api: 'openai-completions', + compat: { thinkingFormat: 'openrouter', sessionAffinityFormat: 'openrouter' }, + }; + } + return { api: protocol, compat: undefined }; +} + +export function buildPiProviderConfig(fixture) { + const mapping = mapMakeloreProtocolToPi(fixture.apiProtocol); + const model = fixture.model ?? {}; + return { + providers: { + [fixture.id]: { + baseUrl: fixture.baseUrl, + apiKey: `$${fixture.apiKeyEnv}`, + api: mapping.api, + ...(fixture.headers ? { headers: fixture.headers } : {}), + models: [{ + id: model.id, + name: model.name ?? model.id, + reasoning: model.reasoning ?? false, + input: model.input ?? ['text'], + cost: model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.contextWindow ?? 128_000, + maxTokens: model.maxTokens ?? 8_192, + ...(mapping.compat ? { compat: mapping.compat } : {}), + }], + }, + }, + }; +} + +async function loadProviderFixture(path) { + const fixture = await readJson(path); + for (const field of ['id', 'apiProtocol', 'baseUrl', 'apiKeyEnv']) { + if (!fixture[field]) throw new Error(`Provider fixture is missing ${field}`); + } + if (!fixture.model?.id) throw new Error('Provider fixture is missing model.id'); + mapMakeloreProtocolToPi(fixture.apiProtocol); + if (!process.env[fixture.apiKeyEnv]) { + throw new Error(`Provider credential environment variable is not set: ${fixture.apiKeyEnv}`); + } + return fixture; +} + +async function imageContentFromPath(path) { + const data = await readFile(path); + const extension = path.toLowerCase().split('.').at(-1); + const mimeType = extension === 'png' ? 'image/png' : extension === 'webp' ? 'image/webp' : 'image/jpeg'; + return { type: 'image', data: data.toString('base64'), mimeType }; +} + +function lastAssistantStopReason(messagesResponse) { + const messages = messagesResponse.data.messages; + return [...messages].reverse().find((message) => message.role === 'assistant')?.stopReason ?? null; +} + +async function runProviderWorker(runtime, paths, fixture, timeoutMs) { + await writeFile( + join(paths.configDir, 'models.json'), + `${JSON.stringify(buildPiProviderConfig(fixture), null, 2)}\n`, + ); + const worker = new PiRpcWorker({ + executable: runtime.electronExecutable, + cliPath: runtime.cliPath, + cwd: paths.cwd, + env: workerEnv(paths.configDir), + args: baseRpcArgs(paths.sessionDir, ['--provider', fixture.id, '--model', fixture.model.id]), + timeoutMs, + }); + await worker.start(); + return worker; +} + +async function promptAndSettle(worker, message, images, timeoutMs) { + const eventStartIndex = worker.events.length; + const sentAt = performance.now(); + const accepted = await worker.request({ type: 'prompt', message, ...(images ? { images } : {}) }); + const agentStart = await worker.waitForEvent( + (event) => worker.events.indexOf(event) >= eventStartIndex && event.type === 'agent_start', + timeoutMs, + ); + const settled = await worker.waitForEvent( + (event) => worker.events.indexOf(event) >= eventStartIndex && event.type === 'agent_settled', + timeoutMs, + ); + const messagesResponse = await worker.request({ type: 'get_messages' }); + return { + acceptedMs: accepted.receivedAt - sentAt, + startedAt: agentStart.receivedAt, + settledAt: settled.receivedAt, + stopReason: lastAssistantStopReason(messagesResponse), + }; +} + +export async function runProviderQualification(runtime, scratchRoot, fixturePath, imagePath, timeoutMs) { + const fixture = await loadProviderFixture(fixturePath); + const [leftPaths, rightPaths] = await Promise.all([ + makeWorkerPaths(scratchRoot, 'provider-left'), + makeWorkerPaths(scratchRoot, 'provider-right'), + ]); + const [left, right] = await Promise.all([ + runProviderWorker(runtime, leftPaths, fixture, timeoutMs), + runProviderWorker(runtime, rightPaths, fixture, timeoutMs), + ]); + try { + const state = await Promise.all([ + left.request({ type: 'get_state' }), + right.request({ type: 'get_state' }), + ]); + await Promise.all([ + left.request({ type: 'set_auto_retry', enabled: false }), + right.request({ type: 'set_auto_retry', enabled: false }), + ]); + const images = imagePath ? [await imageContentFromPath(imagePath)] : undefined; + const [leftTurn, rightTurn] = await Promise.all([ + promptAndSettle(left, 'Reply with exactly PI_PROVIDER_LEFT_OK. Do not use tools.', images, timeoutMs), + promptAndSettle(right, 'Reply with exactly PI_PROVIDER_RIGHT_OK. Do not use tools.', images, timeoutMs), + ]); + const overlapMs = Math.min(leftTurn.settledAt, rightTurn.settledAt) + - Math.max(leftTurn.startedAt, rightTurn.startedAt); + const successfulReasons = new Set(['stop', 'length', 'toolUse']); + if (!successfulReasons.has(leftTurn.stopReason) || !successfulReasons.has(rightTurn.stopReason)) { + throw new Error(`Provider turns did not succeed: left=${leftTurn.stopReason}, right=${rightTurn.stopReason}`); + } + if (overlapMs <= 0) throw new Error('Provider turns did not overlap'); + if (state[0].data.sessionId === state[1].data.sessionId) { + throw new Error('Provider workers shared a session id'); + } + + const abortStartIndex = left.events.length; + const abortedPrompt = left.request({ + type: 'prompt', + message: 'Start a normal response. This turn will be aborted by the qualification probe.', + }); + const unaffectedTurn = promptAndSettle( + right, + 'Reply with exactly PI_PROVIDER_ABORT_ISOLATION_OK. Do not use tools.', + undefined, + timeoutMs, + ); + await abortedPrompt; + await left.waitForEvent( + (event) => left.events.indexOf(event) >= abortStartIndex && event.type === 'agent_start', + timeoutMs, + ); + await left.request({ type: 'abort' }); + await left.waitForEvent( + (event) => left.events.indexOf(event) >= abortStartIndex && event.type === 'agent_settled', + timeoutMs, + ); + const [abortedMessages, unaffected] = await Promise.all([ + left.request({ type: 'get_messages' }), + unaffectedTurn, + ]); + const abortedStopReason = lastAssistantStopReason(abortedMessages); + if (abortedStopReason !== 'aborted') { + throw new Error(`Aborted provider turn ended with ${abortedStopReason ?? 'no assistant message'}`); + } + if (!successfulReasons.has(unaffected.stopReason)) { + throw new Error(`Aborting one provider worker affected the other: ${unaffected.stopReason}`); + } + const stops = await Promise.all([left.stop(), right.stop()]); + + return { + protocol: fixture.apiProtocol, + providerId: fixture.id, + modelId: fixture.model.id, + headerNames: Object.keys(fixture.headers ?? {}).sort(), + apiKeyEnv: fixture.apiKeyEnv, + imageInput: Boolean(imagePath), + distinctSessionIds: true, + overlapMs: Math.round(overlapMs), + promptAcceptedMs: summarizeMeasurements([leftTurn.acceptedMs, rightTurn.acceptedMs]), + stopReasons: [leftTurn.stopReason, rightTurn.stopReason], + abortIsolation: { + abortedStopReason, + unaffectedStopReason: unaffected.stopReason, + passed: true, + }, + exitModes: stops.map((stop) => stop.mode), + }; + } finally { + await Promise.all([ + left.stop().catch(() => undefined), + right.stop().catch(() => undefined), + ]); + } +} + +function printHelp() { + process.stdout.write(`Usage: node scripts/probe-pi-runtime.mjs [options]\n\n`); + process.stdout.write(' --stage Install the package shrinkwrap into a temporary staged runtime\n'); + process.stdout.write(' --keep-stage Preserve the temporary staged runtime for inspection\n'); + process.stdout.write(' --samples Cold and warm startup sample count (default: 5)\n'); + process.stdout.write(' --timeout-ms RPC/provider timeout (default: 10000)\n'); + process.stdout.write(' --report Write the JSON report to this path\n'); + process.stdout.write(' --provider-fixture Opt in to a real-provider overlap qualification run\n'); + process.stdout.write(' --image Add an image to provider prompts (requires fixture)\n'); + process.stdout.write(' --electron-executable

Use a packaged Electron executable (requires --cli-path)\n'); + process.stdout.write(' --cli-path Pi CLI path visible to packaged Electron\n'); + process.stdout.write(' --artifact-label