Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 222 additions & 71 deletions bin/ocx.mjs

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions devlog/_plan/260911_l4_service_cli/000_packet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Dispatch packet — L4 (revision 5)

Round unit: `devlog/_plan/260911_lane_dispatch_round` on `dev`. Base freeze: `origin/dev` `6d3ad12e3` (2.51.0).
Five audit rounds shaped this packet. The last one was a seven-lane feasibility check that asked whether each stack is implementable inside its owned paths; three lanes came back with gaps, and the fixes are folded here. `010_lane_partition.md` is the authoritative ownership list; `130_wp4_feasibility.md` records why each path was granted.


## Shared frame

**Repository.** Your worktree is named in your packet, already checked out on your lane branch, cut
from `origin/dev` `6d3ad12e3` (2.51.0). Work only there. Do not add, move, or remove a worktree.

**Loop.** Run `$codexclaw:cxc-loop` as HOTL for your lane: one work-phase per issue, in order. Your
goal ends when your last PR is green and reported, not when the code looks right.

**Subagents.** Unlimited `xai/grok-4.6` subagents, read-only, spawned with `spawn_agent`
(`model: "xai/grok-4.6"`). Use them to reproduce, to read the call sites you are about to change, to
find a second caller of a helper you are touching, and to review your staged diff adversarially
before you push. A finding enters your work only with an exact `path:line` anchor. Subagents never
write, commit, push, or call a mutating `gh`. Treat a `fail` verdict the way this round did: fold it
in and re-audit. This packet is at revision 3 because two audit rounds rejected revisions 1 and 2.

**MUST NOT.**

- No local product suite: no `bun test`, no `bun run test`, no `bun run test:changed`, no
`bun run typecheck`, no `bun run build:gui`, no `bun install`. Report them as `NOT RUN`.
- No merge, no release, no force-push to a shared branch, no direct push to `dev`.
- No path outside your owned list, including paths a carried PR happens to touch. Dropping a hunk
from a carried PR is expected; report what you dropped.
- No locale key in `gui/src/i18n/*`. If you need one, stop and report.
- No security write-up in `devlog/`; scratch space only, per `AGENTS.md`.

**MUST.**

- Prefix every mutating git command with `git -c core.hooksPath=/dev/null`. This repository's hooks
can start a GUI install, typecheck, and build, which the no-local-suite rule forbids.
- Push with `--no-verify`.
- Write the focused regression test `AGENTS.md` requires for a behaviour change, in the domain
directory beside the existing tests for that subsystem, and register it in both
`scripts/test-layout/layout.json` `explicit` and `tests/fixtures/test-layout-expected.json`. You
will not run it; hosted CI will. Those two maps are append-only and other lanes are adding to them
too; the orchestrator resolves the conflicts at merge, so do not skip the entry.
- Fill every section of `.github/PULL_REQUEST_TEMPLATE.md` and put `Closes #<issue>` in the body. In
**Verification**, state that the local suite, typecheck, and build were `NOT RUN` by operator
instruction and that hosted CI on the exact pushed head is the proof.
- When you carry another author's PR, add a `Co-authored-by` trailer in a branch commit. Resolve the
address with `gh api users/<login> --jq '.id'` and use `<id>+<login>@users.noreply.github.com`.
- Keep a devlog unit under `devlog/_plan/260911_l<N>_<slug>/`.

**Stacking.** First PR targets `dev`; the second targets the first PR's head branch, the third the
second. Retarget a child to `dev` after its parent lands. No native GitHub stacks.

**Decisions already made for you.** Both audit rounds found items where the issue left a real choice
open. Those calls are recorded in your packet in bold. Implement the recorded decision; if you think
it is wrong, report the reason and stop.

**Stop conditions.** Stop and report when the fix needs a path you do not own, when it needs a policy
no issue has fixed, when a locale key is unavoidable, or when hosted CI fails for a reason outside
your diff.

**Report format.** Per PR: number, exact head SHA, CI run id and conclusion, the issue it closes, the
co-authors credited, the hunks you dropped from a carried PR, and any decision you made. Say
`NOT RUN` for local checks.

**Decision boundary.** You do not merge, do not close another author's PR, and do not rank your lane
against another. When your last PR is green, report and stop.

## L4 — service, update, CLI, and connected client

Worktree `~/.codex/worktrees/260911-l4/opencodex`, branch `codex/260911-l4-service-cli`.

Owned: directories `src/update/`, `src/cli/`, `src/client/`; files `bin/ocx.mjs`, `src/cli.ts`,
`src/service.ts`, `src/config/pending-teardown.ts`, `src/lib/bun-runtime.ts`,
`src/lib/package-tree-integrity.ts`, `src/lib/process-control.ts`, `src/codex/catalog/effort.ts`,
`src/codex/cli-install-provenance.ts`, `docs-site/src/content/docs/getting-started/installation.md`.

Your stack is #4202 → #4169 → #4207. **#4204 was removed from the round** by the feasibility audit:
binding the clamp to the Desktop runtime needs `codex/runtime.ts:573`, `catalog/bundled.ts:239`, and
`catalog/sync.ts:1945`, because the catalog probes one selected runtime and no caller passes a
consumer identity. Resolving a catalog per consumer is a design decision this round does not make.

1. **#4202 — global pnpm installations cannot self-update.** Carry PR #4203 by `oliver-mee` (open
**draft**, `CHANGES_REQUESTED`, 36 files). **Decision: the keep-set is exactly** `bin/ocx.mjs`,
`src/cli.ts`, `src/cli/launcher-context.ts`, `src/config/pending-teardown.ts`,
`src/lib/bun-runtime.ts`, `src/lib/package-tree-integrity.ts`, `src/service.ts`, every file under
`src/update/`, the tests `tests/ci-workflows/install-scripts.test.ts`,
`tests/cli/ocx-launcher-runtime.test.ts`, `tests/cli/ocx-launcher-source.test.ts`,
`tests/update/update-badge.test.ts`, `tests/update/update-job.test.ts`,
`tests/update/update-pnpm.test.ts`, `tests/update/update-stop-first.test.ts`, the two test-layout
maps, and `docs-site/src/content/docs/getting-started/installation.md`. **Drop** `README.md`,
`structure/01_runtime.md`, `structure/06_docs-and-release.md`,
`docs-site/src/content/docs/getting-started/for-agents.md`, and
`docs-site/src/content/docs/reference/cli/lifecycle.md`.
2. **#4169 — every stop refusal is reported as a `CODEX_HOME` ownership mismatch,** hiding
`respawnable_service`. Carry PR #4170 by `yeongjunyoo` (open **draft**, `REVIEW_REQUIRED`); it
touches `src/cli/index.ts` and `src/lib/process-control.ts`, both yours, plus its two tests
`tests/lib/process-control-graceful.test.ts` and `tests/providers/xai/grok-lifecycle.test.ts`,
which you keep.
3. **#4204 — Windows: a stale persisted CLI 0.135.0 strips max/ultra while Desktop runs 0.153.4.**
The clamp is `src/codex/catalog/effort.ts:441`. #4178 by `luvs01` is open, not a draft, full CI
green, and owns `src/codex/cli-install-provenance.ts`: if it lands first, rebase onto it;
otherwise keep out of that file and say so.
4. **#4207 — the connected catalog reports success while the local Codex CLI rejects unsupported
reasoning levels.** Same clamp as #4204, which is why both are here; client side is
`src/client/hub-client.ts:145`, `src/client/connect.ts:542`, `src/cli/connect.ts:187`.
**Decision: fail closed — block local readiness rather than reporting success** when the
projection is not compatible with the local client.

88 changes: 88 additions & 0 deletions devlog/_plan/260911_l4_service_cli/010_wp1_pnpm_self_update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# wp1 — #4202: global pnpm installations cannot self-update

Work-phase 1 of the L4 lane. Base: `origin/dev` after #4226 and #4227 landed. Carried source:
PR #4203 by `oliver-mee`, head `e74c6e54d`, two commits on base `f94dd88f1`.

## What the issue asks for

`ocx update` on a pnpm global installation forwards npm-only flags (`--allow-scripts=bun`,
`--no-audit`, `--no-fund`) to pnpm, which rejects them. The failure lands **after** the proxy has
already been stopped. #4202 asks for either a pnpm-native global update path or a safe actionable
error raised before the proxy is stopped.

## Keep-set, verbatim from the packet

31 of the carry's 36 files. Kept: `bin/ocx.mjs`, `src/cli.ts`, `src/cli/launcher-context.ts`,
`src/config/pending-teardown.ts`, `src/lib/bun-runtime.ts`, `src/lib/package-tree-integrity.ts`,
`src/service.ts`, every file under `src/update/`, the seven carried tests, the two test-layout maps,
and `docs-site/src/content/docs/getting-started/installation.md`.

Dropped, because L4 does not own them: `README.md`, `structure/01_runtime.md`,
`structure/06_docs-and-release.md`, `docs-site/src/content/docs/getting-started/for-agents.md`,
`docs-site/src/content/docs/reference/cli/lifecycle.md`.

`git diff f94dd88f1..e74c6e54d` restricted to the keep-set is 3676 lines and
`git apply --check` reports no conflict against the rebased branch: no keep-set path moved on
`dev` between the carry's base and the current tip.

## The blocking finding this work-phase must fold in

`Ingwannu` (repository owner) requested changes on #4203:

> In src/update/transactional-install.mjs, verifyInstallTree now delegates to
> dependencyPackageDir/createRequire.resolve. That resolution can find dependencies in ancestor
> node_modules outside the candidate package tree. […] A candidate missing its own bundled Bun or
> sentinel dependency must not pass merely because an ancestor installation supplies one; otherwise
> staging/boot recovery can call a non-self-contained candidate healthy and discard or replace the
> known-good copy.

The finding is structural, not stylistic. In the carry both exported verifiers are the same
function: `verifyInstallTree` and `verifyPnpmInstallTree` each call
`verifyInstallTreeWithDependencyRoot`, which resolves `bun` and the sentinel deps through
`createRequire(...).resolve`. Node's resolution walks the ancestor directory chain, so for a global
npm layout a candidate at `<prefix>/lib/node_modules/@bitkyc08/opencodex` can satisfy its bun
requirement from `<prefix>/lib/node_modules/bun`, which belongs to a different package.

Three decisions consume that boolean, all on the npm path:

- `transactional-install.mjs:198` accepts the staged tree (D2, before the swap).
- `transactional-install.mjs:241` re-verifies the live tree after the swap and decides rollback.
- `transactional-install.mjs:121`, inside `bootRestoreProbe`, decides that the live tree is healthy
and **reaps every backup**, which is the only known-good copy.

So an over-permissive verdict is not cosmetic: it can accept a stage that cannot start, then delete
the backup that would have recovered it.

## Plan

1. Apply the keep-set diff unchanged.
2. Split the verifier in `src/update/transactional-install.mjs` into two real implementations that
share the manifest checks but not the dependency-resolution policy:
- `verifyInstallTree` (npm, and every caller above) returns to the strict pre-carry rule: a
sentinel dependency counts only at `<packageDir>/node_modules/<name>/package.json`, and the
bun size gate reads only `<packageDir>/node_modules/bun`. Ancestor resolution cannot satisfy it.
- `verifyPnpmInstallTree` keeps resolver-based discovery, because pnpm legitimately exposes
dependencies through a virtual store, a package-root symlink, or a hoisted group root, but adds
the ownership check the review asked for: the resolved dependency must live under a dependency
root that this package instance owns — its own `node_modules`, its realpath's `node_modules`,
or the `node_modules` that encloses the package when that root carries pnpm's own metadata
(`.pnpm` or `.modules.yaml`). An ancestor root with no pnpm evidence is refused.
3. Regression test at `tests/update/update-tree-ownership.test.ts`, registered in
`scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`, covering the
three cases the review named: a candidate missing its own bun with an unrelated ancestor bun
present, a truncated candidate bun with an intact ancestor bun, a legitimate pnpm virtual-store
and hoisted layout, and the `bootRestoreProbe` decision — the probe must restore the backup
rather than reap it when the live tree is not self-contained.
4. Commit with a `Co-authored-by` trailer for `oliver-mee`, push with `--no-verify`, open the PR
against `dev` with `Closes #4202`.

## Diff level

`src/update/transactional-install.mjs` ~60 lines changed on top of the carry; one new test file;
two one-line map registrations. Everything else is the carry verbatim.

## Not run

`bun test`, `bun run test`, `bun run test:changed`, `bun run typecheck`, `bun run build:gui` and
`bun install` are NOT RUN by operator instruction. Hosted CI on the exact pushed head is the only
product evidence this round accepts.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vision and web-search sidecars can also use your ChatGPT login when a routed mod

| Requirement | Why |
| --- | --- |
| **[Node](https://nodejs.org) ≥ 18** | `ocx` runs on the Bun runtime, but the runtime is bundled automatically on `npm install` — you do **not** need to install Bun yourself. |
| **[Node](https://nodejs.org) ≥ 18** | `ocx` runs on the Bun runtime, but the runtime is bundled automatically by the npm or pnpm install — you do **not** need to install Bun yourself. |
| **[OpenAI Codex](https://openai.com/codex)** (CLI, App, or SDK) | The client opencodex sits in front of. opencodex writes to `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`). |
| A provider account or API key | Anthropic, xAI, Kimi, Ollama Cloud, OpenRouter, an OpenAI-compatible endpoint, or your ChatGPT login. |

Expand All @@ -21,6 +21,12 @@ vision and web-search sidecars can also use your ChatGPT login when a routed mod
npm install -g @bitkyc08/opencodex
```

With pnpm 10.4 or later:

```bash
pnpm add -g --allow-build=bun @bitkyc08/opencodex
```

:::note[npm blocked the bun postinstall?]
Recent npm versions may block bun's postinstall script (`npm warn
install-scripts ... blocked because they are not covered by allowScripts`),
Expand Down
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1232,10 +1232,12 @@
"update-notify.test.ts": "update",
"update-npm-cache-preflight.test.ts": "update",
"update-npm-invocation.test.ts": "update",
"update-pnpm.test.ts": "update",
"update-stop-classification.test.ts": "update",
"update-stop-first.test.ts": "update",
"update-transactional.test.ts": "update",
"update-tray-handoff.test.ts": "update",
"update-tree-ownership.test.ts": "update",
"upstream-connect-error.test.ts": "server",
"upstream-http-error.test.ts": "adapters",
"upstream-http-version.test.ts": "server",
Expand Down
4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//
// Before the src/ restructure the CLI lived at src/cli.ts, and durable launchers
// (codex shim wrappers, installed service definitions) baked that absolute path
// into their command lines. Users who upgrade in place with a bare
// `npm install -g @bitkyc08/opencodex` (instead of `ocx update`, which repairs the
// into their command lines. Users who upgrade in place with a bare package-manager
// install (`npm install -g` or `pnpm add -g`) (instead of `ocx update`, which repairs the
// shim/service) would otherwise be stranded on a dead path. Keep this stub for at
// least one release cycle after the restructure ships.
import "./cli/index.ts";
2 changes: 1 addition & 1 deletion src/cli/launcher-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Trusted facts captured by the plain-Node npm launcher before Bun auto-loads
* Trusted facts captured by the plain-Node package launcher before Bun auto-loads
* project dotenv files. The random proof travels in argv while the context
* travels in the environment, so a project `.env` cannot forge the pair during
* an ordinary `ocx ...` invocation.
Expand Down
2 changes: 1 addition & 1 deletion src/config/pending-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export function listPendingTeardowns(): OutstandingTeardown[] {
}
const out: OutstandingTeardown[] = [];
for (const name of names) {
// One naming rule, shared with the npm launcher: the two lanes drifting apart is
// One naming rule, shared with the package launcher: the two lanes drifting apart is
// exactly how the Node updater stopped seeing receipts at all.
if (!isPendingTeardownFileName(name)) continue;
const nonce = pendingTeardownNonceFromFileName(name)!;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bun-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function durableBunRuntime(): DurableBunRuntime {
/**
* Bun path to bake into durable artifacts (launchd/systemd/Task Scheduler and
* the Codex auto-start shim). Prefer the bundled binary — it lives under the
* npm global prefix and survives across `ocx update` — and fall back to the
* manager-owned global package directory and survives across `ocx update` — and fall back to the
* current runtime, which is Bun when launched normally.
*/
export function durableBunPath(): string {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/package-tree-integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface PackageTreeIntegrityGuard {
}

type ObservePackageTree = () => PackageTreeObservation | null;
type PackageTreeRuntimeInstall = "bun" | "npm" | "source";
type PackageTreeRuntimeInstall = "bun" | "npm" | "pnpm" | "source";

const packageManifestUrl = new URL("../../package.json", import.meta.url);

Expand Down
2 changes: 1 addition & 1 deletion src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const TASK = "opencodex-proxy";
export type ServiceBackend = "scheduler" | "native";

function cliEntry(runtime: DurableBunRuntime = durableBunRuntime()): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
// Bake the bundled Bun (npm global prefix, survives `ocx update`) rather than
// Bake the bundled Bun (manager-owned global package directory, survives `ocx update`) rather than
// a transient system Bun, so launchd/systemd/schtasks keep resolving even if a
// standalone Bun is later removed. The CLI entry lives at src/cli/index.ts.
//
Expand Down
5 changes: 3 additions & 2 deletions src/update/badge.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* Cached "is an update available?" answer for the GUI sidebar badge.
*
* `/api/update/check` spawns `npm view` on every call (~1s, network-bound), so a
* `/api/update/check` spawns the installing manager's `view` command on every call
* (~1s, network-bound), so a
* sidebar that polls it would spawn a process per tick on every page of the GUI.
* The badge instead READS the 20h version cache the CLI update prompt already
* maintains (`~/.opencodex/version.json`).
*
* This is deliberately read-only: it must never trigger a registry refresh. The GUI
* polls it, so a refresh-on-read would let repeated polls launch repeated `npm view`
* polls it, so a refresh-on-read would let repeated polls launch repeated manager `view`
* helpers with no coalescing. Cache warming stays with `ocx start`
* (`triggerBackgroundRefreshIfStale` in `src/update/notify.ts`) and with the explicit
* `/api/update/check` the user reaches by clicking the sidebar update button.
Expand Down
Loading
Loading