Skip to content

feat: bundle pi so the backend works with zero install - #141

Merged
saucam merged 2 commits into
mainfrom
feat/pi-bundled
Jul 10, 2026
Merged

feat: bundle pi so the backend works with zero install#141
saucam merged 2 commits into
mainfrom
feat/pi-bundled

Conversation

@saucam

@saucam saucam commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What

Makes the pi backend seamless. Previously pi required a separate npm i -g — the catalog advertised it regardless, and a missing binary only surfaced as a spawn error on the user's first turn.

How

  • Bundled, pinned: @earendil-works/pi-coding-agent@0.80.6 becomes an optionalDependency. The exact pin is a safety property, not just convenience — the injected approval bridge (fail-closed tool gating) is coupled to pi's RPC + extension API, and the lockfile freezes precisely the pi version the bridge was tested against. A system install or explicit providers.pi.command still wins for users who want their own build; they own that pairing's compatibility.
  • Startup resolution (pi/resolve.ts), first match wins: explicit providers.pi.command (verified — a typo'd path is loud at startup, not a first-turn failure) → system pi on PATH → the bundled copy, spawned via the daemon's own runtime (process.execPath + cli entry, so the fallback doesn't additionally require node on PATH). PiRpcProcess gains an argsPrefix that precedes --mode rpc for the bundled form.
  • Truthful catalog: ProviderRegistry learns supported-but-unavailable backends (markUnavailable / unavailableHint). When nothing resolves, pi stays out of the switchable set, the daemon logs the fix at startup, and session.set_provider answers "pi" is supported but not available: <install hint> instead of a bare "unknown provider". This is generic on purpose — the upcoming codex provider reuses it.
  • No wire changes.

Tests

Resolution order (config hit/miss, bare-name PATH lookup, system-beats-bundled, bundled fallback shape), a live smoke that runs the bundled pi under the daemon runtime (--version from node_modules, no network — this CI machine has no system pi, so the zero-install path is what's actually exercised), registry activation/unavailable/disabled states, and the switch-error hint path.

bun run test (1236 pass), typecheck, lint green.

🤖 Generated with Claude Code

pi previously required a separate global install; a missing binary
surfaced only as a spawn error on the user's first turn. Now:

- @earendil-works/pi-coding-agent is a pinned optionalDependency. The
  pin is a safety property, not just convenience: the injected approval
  bridge is coupled to pi's RPC + extension API, and the lockfile
  freezes exactly the pi version the bridge was tested against.
- pi/resolve.ts resolves the binary ONCE at startup: explicit
  providers.pi.command (verified — a typo'd path is loud at startup,
  not a first-turn failure) → system pi on PATH → the bundled copy run
  via the daemon's own runtime (process.execPath + cli entry, so the
  fallback doesn't additionally require node). PiRpcProcess gains an
  argsPrefix that precedes --mode rpc for the bundled form.
- ProviderRegistry learns supported-but-unavailable backends
  (markUnavailable/unavailableHint): when nothing resolves, pi stays
  out of the switchable catalog but session.set_provider answers with
  the actionable install hint instead of 'unknown provider', and the
  daemon logs the fix at startup.

Tests: resolution order (config hit/miss, bare-name PATH lookup, system
beats bundled, bundled fallback), a live smoke that RUNS the bundled pi
under the daemon runtime (--version, no network), registry activation/
unavailable/disabled states, and the switch-error hint path.
Comment thread src/tests/pi-resolve.test.ts Outdated
@github-actions

Copy link
Copy Markdown

🤖 Gemini code review

Bundles pi as a pinned optional dependency to enable zero-install usage out of the box. Implements a startup-time resolution strategy (explicit config -> system PATH -> bundled fallback) to fail-fast with actionable installation hints instead of failing on the first user turn.

Findings: 🔴 0 · 🟠 0 · 🟡 0 · 🟢 1

Ignored 1 file(s) via ignore globs.


Tokens spent · ⬆️ Input: 6,773 · ⬇️ Output: 209 · Σ Total: 11,254
Total may be higher due to thinking token counts.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.71429% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.11%. Comparing base (4d37333) to head (1b35a92).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/daemon/providers/pi/resolve.ts 93.54% 2 Missing ⚠️
src/daemon/session-manager.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #141      +/-   ##
==========================================
+ Coverage   82.07%   82.11%   +0.04%     
==========================================
  Files          97       98       +1     
  Lines       16867    16922      +55     
==========================================
+ Hits        13843    13895      +52     
- Misses       3024     3027       +3     
Flag Coverage Δ
daemon 82.11% <95.71%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/daemon/providers/pi/index.ts 90.33% <100.00%> (+0.04%) ⬆️
src/daemon/providers/pi/rpc.ts 96.90% <100.00%> (ø)
src/daemon/providers/registry.ts 100.00% <100.00%> (ø)
src/daemon/session.ts 95.44% <100.00%> (+<0.01%) ⬆️
src/daemon/session-manager.ts 65.49% <50.00%> (-0.02%) ⬇️
src/daemon/providers/pi/resolve.ts 93.54% <93.54%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@saucam
saucam merged commit 3bc25c3 into main Jul 10, 2026
4 checks passed
saucam added a commit that referenced this pull request Jul 10, 2026
…lice) (#142)

* feat: OpenAI Codex as a codeoid backend (app-server JSON-RPC, first slice)

Codex CLI becomes the third native harness backend, riding ChatGPT-
subscription auth that lives entirely in codex's own ~/.codex store.
Grounded against a live probe of @openai/codex@0.144.1 app-server
(newline JSON-RPC over stdio; see docs/provider-codex-design.md).

- codex/rpc.ts: JSON-RPC client — requests, notifications, and
  SERVER→client requests (the native approval seam; unlike pi, no
  injected bridge extension is needed).
- codex/index.ts: CodexProvider (SessionProvider) — warm process, one
  codex thread per backing session (thread id = backingSessionId,
  thread/resume on reconnect, fresh thread on rotation), turn/start
  pinned to approvalPolicy 'untrusted' so codex ASKS before privileged
  actions and codeoid's canUseTool is authoritative (fail-closed: no
  gate wired → denied; unknown server requests → refused).
  item/tool/requestUserInput maps to requestUserInput
  (session.ui_request). Non-gated items (codex-trusted reads) are
  recorded retrospectively as paired tool_start/tool_complete so
  scrollback + canonical history stay honest. systemPromptAppend →
  developerInstructions; seedFromHistory prepends the structured
  transcript (warm-backend ceiling). model/list → live catalog.
- buildCodexEnv: hardened subprocess env (CODEX_/OPENAI_/LC_ prefixes,
  *_API_KEY suffix; ~/.codex via HOME) — GHSA-38vh posture.
- Config providers.codex {enabled, command}; registry activation via
  resolveCodexCommand (config → PATH; no bundled fallback yet — native
  per-platform binary, needs a size audit) with the #141
  supported-but-unavailable hint otherwise.

Tests: fake-codex fixture (newline JSON-RPC subprocess) driving C1-C9:
text/thinking streaming + usage, approval approve/deny round-trips,
retrospective non-gated items, structured history seed (one-shot),
user-input select round-trip, missing binary, model mapping, resolution
+ registry hints. No wire changes.

* fix: guard codex stdin against EPIPE + import randomUUID explicitly (Gemini review)

* fix: close codex coverage gaps flagged by codecov patch report

Patch coverage 89.4% → ~99%: C9 lifecycle (thread id round-trips as the
backing id, reset/setHasQueried/dispose, onModels catalog), C10
interrupt → turn/interrupt → stopReason 'interrupted', C11 unknown
server→client request refused with a JSON-RPC error (fail closed), C12
rpc edges (spawn failure rejects in-flight requests, request timeout,
request/notify after exit), plus bare-name and system-PATH codex
resolution. fake-codex grows hang-forever/unknown-request/noReply
behaviors and surfaces error responses to turn scripts.
saucam added a commit that referenced this pull request Jul 10, 2026
…ription auth) (#143)

* feat: gemini-cli as a codeoid backend over ACP (bundled, Google-subscription auth)

Fourth native harness backend: Google's gemini-cli driven over ACP (the
Agent Client Protocol — standardized editor<->agent JSON-RPC/stdio;
gemini-cli exposes it via the stable --acp flag). Auth is the user's
Google-account OAuth in ~/.gemini (free tier / AI Pro / Code Assist) —
subscription posture like claude/codex; codeoid never touches tokens.

- providers/jsonrpc-stdio.ts: the codex transport generalized into a
  shared StdioJsonRpcProcess (codex/rpc.ts is now a thin wrapper baking
  in the app-server subcommand — all codex tests unchanged).
- acp/index.ts: GeminiAcpProvider — session/new is the backing session,
  session/prompt resolves at turn end (stopReason -> turn_done; honest
  zero usage, ACP carries none), session/update chunks -> text/thinking
  deltas + tool_call/tool_call_update -> tool records,
  session/request_permission -> canUseTool (deny picks the reject
  option; fail-closed: no gate or no reject option -> cancelled;
  fs/terminal capabilities DECLINED at initialize and any stray request
  refused), session/cancel -> interrupt. Deliberately protocol-thin: any
  ACP agent can reuse the shape with a different binary.
- Bundled: @google/gemini-cli@0.80->0.50.0 pinned optionalDependency
  (pure JS, runs under the daemon runtime — live smoke in tests);
  resolution config -> PATH -> bundled, with the #141 unavailable-hint
  path. buildGeminiCliEnv allowlist (GEMINI_/GOOGLE_/LC_ + *_API_KEY).
- Config providers.geminiCli {enabled, command}; provider id gemini-cli
  (distinct from the stateless gemini API backend). No wire changes.

Tests: fake-acp fixture (long-running session/prompt answered at turn
end, permission options, cancellation) driving A1-A9 + resolution/
registry states + live bundled smoke. Registry catalog test updated for
the bundled backend.

* fix: tolerate synchronous stdin write failures on a dead child (Gemini review)

write() on a destroyed stream throws synchronously — the #exited flag
and the stdin error listener only cover the async paths. All frame
writes now go through a #write helper that swallows the throw (exit
handler owns diagnostics) and routes request-path write errors to the
pending rejection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant