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
88 changes: 88 additions & 0 deletions devlog/_plan/260912_omo_client_integration/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# omo as an export and integration client

## What this unit adds

`omo-ai@beta` (5.0.0-0.beta.53, bin `omo`, repo `code-yeongyu/oh-my-openagent`)
describes itself as "omo native edition - the senpi-based OMO harness". It keeps
engine state under `~/.omo/agent` and its allowlist of carried-forward state
files names `models.json` — the same custom-provider catalog Pi, Prime and Aside
read. The user asked for its preset to appear on the Integrations page beside
the other thirteen clients, with a real brand mark.

This unit registers `omo` as the fourteenth export client and the fourteenth
file integration, reusing the Pi builder the way `prime` and `aside` already do
rather than restating the document shape a fourth time.

## Why the Pi family is the right precedent

`prime` is documented in `src/clients/config-export.ts` as "the pi coding agent
shipped under a different brand": it derives its config directory and env prefix
from its own `piConfig` block, so `models.json` is the same contract. omo reaches
the same place by a different route — it is a harness around
`@code-yeongyu/senpi`, written by the same author as Pi — so the claim has to be
verified against senpi's parser rather than assumed from the family
resemblance. `001_omo_contract.md` carries that evidence; the builder is only
reused where the bytes are verified to be accepted.

## Path resolution

omo publishes its own precedence in `bin/lib/agent-dir.js`:
`OMO_CODING_AGENT_DIR`, then `SENPI_CODING_AGENT_DIR`, then
`PI_CODING_AGENT_DIR`, then `~/.omo/agent`. The third entry is Pi's variable and
is deliberately honored by omo itself, so mirroring the chain is reporting omo's
contract, not inventing a shared one. Relative overrides are refused for the
reason MCode, ZCode, Pi and Prime already refuse them: a background proxy and a
foreground client can have different working directories and would otherwise
disagree about which file is named.

## Work phases

| id | phase | contents |
|----|-------|----------|
| wp1 | roadmap | this unit: contract evidence, registration checklist, mark provenance, per-phase docs |
| wp2 | registration | the whole atomic change: path helpers, `EXPORT_CLIENTS.omo`, contribution, `INTEGRATION_CLIENTS.omo`, CLI help and count, catalog-refresh fan-out, every GUI list and record, the mark wiring, nine locales, and every test literal and allowlist that moves with them |
| wp3 | GUI verification | build and serve this worktree's GUI, confirm the row, tab and mark render, and copy-edit the semantics prose against what the page actually shows |
| wp4 | docs | `docs-site` agents reference and integrations guide, English plus translated locales |
| wp5 | verification | typecheck, focused tests, GUI build, and the rendered dashboard proving the row and tab exist |
| wp6 | gjc rename | the Gajae Code label becomes `gjc` on every user-visible surface, with the `gajae` id untouched |

wp3, wp4 and wp6 each depend on wp2; wp5 depends on wp3 and wp4.

**Why wp2 is one phase and not two.** The first plan split backend from GUI.
Two audit rounds failed it on the same ground.
`tests/gui/integrations-invariants.test.ts` asserts sorted equality between
`EXPORT_CLIENT_IDS` and five lists, three of which live in `gui/src`; the GUI
test literals and the two translation allowlists hang off the same edit; and CI
runs `cd gui && bun test` unconditionally. There is no ordering of the halves
that leaves the tree green at the boundary, so registration is one change.

## Scope boundaries

In scope: registration of one new client id across the surfaces
`002_registration_checklist.md` enumerates, plus its brand mark and its docs
rows.

Out of scope: any change to how the Pi document is built for the existing
clients; any new remote-bind credential path; any change to the Integrations
page layout or to the journal/rollback machinery; publishing, releasing, or
pushing anything.

Also out of scope, deliberately: renaming the `gajae` client **id**. wp6 changes
what the user reads, not what the system keys on. The id is the segment in
`/api/client-integrations/gajae` and the key an enable record is filed under, so
renaming it would orphan the stored state of anyone who already connected that
client and leave our ownership record unable to match the block it wrote. The
user asked for the short name and chose the label-only scope.

## Terminal outcomes

DONE requires all five criteria in the bound goalplan to hold with fresh proof:
the row and tab render on the running dashboard with a real mark, the exported
document matches the schema omo parses at the path omo resolves, `bun run
typecheck` is clean, every exact-list test passes with omo included plus a new
omo test, and an enable/disable round trip writes and removes only the owned
fragments.

BLOCKED is the outcome if senpi turns out to reject the Pi document and no
honest mapping exists. NEEDS_HUMAN is the outcome if omo publishes no usable
first-party mark and the user wants something other than the monogram fallback.
96 changes: 96 additions & 0 deletions devlog/_plan/260912_omo_client_integration/001_omo_contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# What omo actually reads

Evidence: `omo-ai` version `5.0.0-0.beta.53` unpacked at `/tmp/omoprobe2/package`,
and `@code-yeongyu/senpi` version `2026.9.10-2` unpacked at
`/tmp/senpiprobe/package`. Schema
claims below were executed against that tarball's own TypeBox compiler
(`typebox@1.3.18`), not read off documentation.

## omo parses nothing

`bin/omo.js` either runs setup or `runLauncher()`, and the launcher brands senpi
and spawns it. The catalog is senpi's. Branded with `configDir: ".omo"` and
`flatLayout: false`, senpi resolves `getModelsPath() = join(getAgentDir(),
"models.json")` (`senpi dist/config.js:506-508`), which with omo's default agent
directory (`bin/lib/agent-dir.js:41-43`) is `~/.omo/agent/models.json`.

omo's own setup only *inspects* that path — `setup-detect.js:139-143` lists it in
`detectedFilePaths`, and `setup-models.js:19` tells the user to define the
provider `baseUrl` in it. Setup writes `auth.json`, never `models.json`, so
opencodex is not fighting omo's installer for the file.

## Path precedence

The launcher resolves, first non-empty trimmed value winning:
`OMO_CODING_AGENT_DIR`, `SENPI_CODING_AGENT_DIR`, `PI_CODING_AGENT_DIR`, else
`~/.omo/agent` (`agent-dir.js:15,33-38`). It then *overwrites* the first two with
the resolved absolute path before spawning senpi, so senpi's own lookup — which
reads the same three names in the same order (`brand.js:126-140`,
`config.js:456-457`) — always finds the launcher's answer and never reaches its
project-local `.omo/agent` walk.

Home is `env.HOME || env.USERPROFILE || os.homedir()` (`agent-dir.js:27-30`).

That third variable is Pi's. omo honors it deliberately, so `omoAgentDir` reading
it is reporting omo's contract rather than asserting a shared one.

## The provider block senpi validates

`providers` is a keyed object; each provider's `models` is an **array** whose
identity is `id`, not a keyed object — a keyed object fails with `must be array`
(`model-config-schema.js:201-220`). That is Pi's shape, not OpenCode's.

Accepted provider keys include `baseUrl`, `apiKey`, `api`, `headers`
(`Record<string, string>`), `compat`, `models`, and `modelOverrides`. `api` is a
bare string at schema time rather than an enum; an unknown value loads and then
fails at stream time (`provider-composer.js:274-277`). `openai-completions` is a
known api (`types.d.ts:25`) and validates.

`thinkingLevelMap` accepts exactly the keys the Pi builder emits — `off`,
`minimal`, `low`, `medium`, `high`, `xhigh`, `max` — each `string | null`
(`model-config-schema.js:65-72`), so emitting `max: "ultra"` as a *value* is
fine.

`input` accepts `text`, `image`, `video` (`model-config-schema.js:170`). `audio`
is rejected — and rejection is not local: a schema failure empties the whole
`models.json` snapshot (`model-config.js:483-487`), so one bad row takes every
custom provider down. This is Pi's failure mode exactly, and it is why
`buildPiClientConfig` drops an audio-only row instead of claiming `text` for it.

`cost` is optional, but a *partial* `cost` is a schema failure: all four rates
are required (`model-config-schema.js:141-155`). The Pi builder omits `cost`
entirely, which is the safe side of that line.

## Verdict

**The bytes `buildPiClientConfig` emits are accepted verbatim.** Nothing needs
renaming, adding, or removing. That was confirmed by running the document
through senpi's compiled validator, not inferred from the family resemblance.

`compat.sendSessionAffinityHeaders` also validates
(`model-config-schema.js:111,210`), so omo takes the builder's
`sendSessionAffinityHeaders` flag as `true`, the way `pi` does and `prime` and
`aside` do not.

## Why omo is still loopback-only

senpi's provider block *does* accept `headers`, and it interpolates `$ENV` and
`${ENV}` in values (`provider-api-key-auth.js:107-117`), so unlike Aside there is
somewhere an `x-opencodex-api-key` could live. What does not exist is a builder
that emits it: `buildPiClientConfig` writes no `headers` at all
(`src/clients/config-export.ts:854-866`), which is exactly why `pi` itself is
loopback-only.

So `loopbackOnly: true` for omo is OMP's and Prime's stance rather than Aside's:
the field exists, the remote credential wiring is deferred, and a non-loopback
bind refuses instead of generating a config that 401s. Adding `headers` to the
shared Pi builder would change four clients at once and is out of this unit's
scope.

## Left unverified

- Whether the dummy `apiKey` is copied into an `Authorization` header at stream
time (would need `pi-ai/dist/api/openai-completions.js`). Irrelevant for a
loopback bind, which admits without a key.
- `oauth: "radius"` appears in senpi's `docs/models.md:139` but not in the live
schema. Not used here.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Every surface a new export client must reach, as of 2026-09-12

The Aside unit wrote this list on 2026-08-31
(`devlog/_fin/260831_aside_client_and_integrations_ux/002_registration_checklist.md`).
Two things have moved since: `tests/` was reorganised into domain directories,
and `raycast` landed as the thirteenth client, with its own app-side status block.
The table below is re-derived against the current tree by word-boundary search
for `raycast`, the freshest template.

## Backend

| file | what omo needs | how failure shows |
|---|---|---|
| `src/clients/config-export/contracts.ts` | `"omo"` in `ExportClientId` | typecheck, everywhere |
| `src/clients/config-export.ts` | `omoAgentDir`, `omoConfigPath`, `buildOmoContribution`, `EXPORT_CLIENTS.omo` | typecheck |
| `src/integrations/registry.ts` | `INTEGRATION_CLIENTS.omo` | typecheck |
| `src/cli/registry.ts` | the `export` usage union and the prose summary | no exact-list gate; `tests/cli/cli-help.test.ts` checks only a prefix of the union |
| `src/cli/help.ts:84` | the `(13 clients)` literal | `tests/cli/cli-help.test.ts:79` |

The count literal lives in `help.ts`, not `registry.ts`, hand-written on purpose
so `ocx --help` does not import the export registry.
`tests/cli/cli-help.test.ts:79` asserts it in lockstep with
`EXPORT_CLIENT_IDS.length`, so it is a focused-test obligation rather than a
cosmetic edit.

`/api/client-config` is served from `src/server/management/model-routes.ts:516`,
not `config-routes.ts` as the 2026-08-31 doc says; it reads the registry and
needs no per-client edit. `ExportClientId` likewise moved to
`src/clients/config-export/contracts.ts:84`.

`src/integrations/ownership-policy.ts`, `state.ts` and `writer.ts` need nothing:
the first is a `zcode`-only exception, and the other two read the registry.
`bun run skill:surface` is not implicated — a new `--client` value creates no
capability.

## GUI

| file | what omo needs | how failure shows |
|---|---|---|
| `gui/src/components/apikeys-workspace/client-config-clients.ts` | `CLIENTS`, `CLIENT_LABEL_KEYS`, `CLIENT_MARKS`, possibly `MONOCHROME_CLIENT_MARKS` | invariant test + typecheck |
| `gui/src/components/integration-marks.ts` | `INTEGRATION_MARKS.omo` | typecheck (exhaustive record) |
| `gui/src/pages/integrations/integration-api.ts` | `INTEGRATION_CLIENT_IDS` | invariant test |
| `gui/src/pages/integrations/integration-tabs.ts` | `TABS` and `FILE_CLIENTS` | **silent** — only `gui/tests/integrations-tab-coverage.test.ts` |
| `gui/src/pages/integrations/overview-clients.ts` | `FILE_LABEL_KEY` | typecheck |
| `gui/src/pages/integrations/FileIntegrationPage.tsx` | `SEMANTICS_KEY`, `TAB_LABEL_KEY`, `FILE_INTEGRATION_CLIENTS` | typecheck + invariant test |
| `gui/src/app-routing.ts` | `integrations/omo` hash | **silent** |
| `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,zh,zh-TW}.ts` | three keys each | `locale-parity` |

## Docs

`docs-site/src/content/docs/reference/cli/agents.md` plus `fr`, `ja`, `ko`, `ru`,
`tr`, `zh-cn`, `zh-tw`; `docs-site/src/content/docs/guides/integrations.md` plus
`fr`, `tr`, `zh-tw`.

## Tests that fail until updated

`tests/config/client-config-export.test.ts`,
`tests/config/client-config-export-new-clients.test.ts`,
`tests/gui/integrations-invariants.test.ts`,
`tests/clients/integrations-state.test.ts`,
`tests/clients/sync-client-integrations.test.ts`,
`tests/clients/integrations-merge.test.ts`,
`tests/cli/cli-export-command.test.ts`,
`tests/cli/cli-headless-parity.test.ts`,
`tests/server/management-integration-routes.test.ts`,
`tests/server/management-client-config-route.test.ts`,
`gui/tests/{client-config-panel.test.tsx,integrations-api.test.ts,integrations-overview-rows.test.ts,integration-marks.test.ts,client-marks-assets.test.ts,locale-parity.test.ts,fr-localization.test.ts,integrations-tab-coverage.test.ts}`.

Of these, the ones verified to hardcode a list or a count are:
`tests/config/client-config-export.test.ts:812` (ordered thirteen ids),
`tests/config/client-config-export-new-clients.test.ts:68` and
`tests/clients/integrations-state.test.ts:799` (loopback-only set),
`tests/gui/integrations-invariants.test.ts:94` (`toHaveLength(13)`, plus the
typechecked `SEED` record), `tests/cli/cli-help.test.ts:79` (the client count),
`gui/tests/integrations-api.test.ts:20` and
`gui/tests/client-config-panel.test.tsx:174` (GUI literals), and
`gui/tests/integrations-overview-rows.test.ts:293` (row count 18 to 19 — the
overview carries five non-file rows on top of the clients).

`gui/tests/integrations-tab-coverage.test.ts` still exists and still reads
`TABS`, `FILE_CLIENTS` and the routable hashes, so the 2026-08-31 silent hole is
covered — conditional on `FILE_INTEGRATION_CLIENTS` being updated, since that is
what the coverage test compares against.

## New files

`tests/clients/omo-client.test.ts` needs an entry in both
`scripts/test-layout/layout.json` `explicit` and
`tests/fixtures/test-layout-expected.json`, or `tests/test-layout-tooling.test.ts`
names the missing one.

The explicit entries are not optional here: the `clients` domain regex does not
match an `omo-` prefix, so the seed cannot place the file. Model the test on
`tests/clients/prime-client.test.ts` rather than the Aside one — Aside's test
lives under `providers` because of that same regex, which would be the wrong
precedent to copy.

## Deliberately not copied from the neighbours

Aside's profile machinery (`aside-profile-*`, the per-profile journal routes) and
Raycast's app-side install block and live-server export branch are client-specific
surfaces, not part of registration. omo has neither.

## One list that is a judgement, not a checklist item

There is not one owned-catalog fan-out list, there are four, and they disagree:

| call site | list | what triggers it |
|---|---|---|
| `src/integrations/catalog-refresh.ts` default, used by `model-routes.ts` | `pi, aside, raycast` | model visibility changed from the dashboard |
| `src/server/management/config-routes.ts:240` | `mcode, pi, aside, raycast` | the `/api/sync` route |
| `src/cli/dispatch.ts:455` | `mcode, pi, raycast` | `ocx sync` (Aside follows immediately through its own server owner) |
| `src/cli/index.ts` | `raycast` | proxy start, via a helper literally named `refreshOwnedRaycastCatalog` |

`prime` is in none of them. Updating one and not the others is the failure
mode, so the decision is taken here rather than in passing.

**Decision: omo joins the three general fan-outs — the `catalog-refresh`
default, `config-routes.ts`, and `dispatch.ts` — and not the fourth.** The
refresh only touches clients that are *already connected*
(`catalog-refresh.ts:10`), so this costs a user who never enables omo exactly
nothing, and it is what stops a connected omo catalog going stale the moment the
user changes model visibility. The fourth is not a general list at all: it is a
Raycast-specific startup helper, and adding an unrelated client to it would
write a file on every `ocx start` for no reason.

`tests/clients/sync-client-integrations.test.ts:68` pins the `config-routes.ts`
list as source text and moves with it.

That `prime` is in no list looks like an oversight from when it landed. Fixing
it is not this unit's business; it is recorded here so the next person does not
read prime's absence as a deliberate pattern to copy.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# The omo mark

## Source

`gui/public/provider-icons/omo.svg` is `https://omo.dev/brand/omo-mark.svg`
unmodified: 4021 bytes, MD5 `c33f72d7c4612c290834ba860f644557`,
`viewBox="0 0 1024 1024"`. The identical file is committed as
`.github/assets/omo-icon-light.svg` in `code-yeongyu/oh-my-openagent` and
rendered as that README's logo, so the same artwork is both the site header mark
and the repository logo. First-party either way.

**Correction.** The first draft of this doc cited the GitHub raw path on `main`.
That URL 404s: the repository's default branch is `dev`. The asset is real and
the bytes match — verified by downloading both and comparing MD5 — but the
branch in the citation was wrong, which is the kind of unreproducible provenance
the README exists to prevent. Both citations now name a URL that resolves.

The project's SUL-1.0 licence says trademark use is "subject to applicable law"
and imposes no distribution ban of the kind that disqualified an earlier
candidate elsewhere in this directory.

## Rejected candidates

- `https://omo.dev/icon.svg` — a single `<text>O</text>` glyph. The client-mark
asset test refuses `<text>`, the same rule that sent Hermes to a trace.
- `omo-logo.png` — a superseded 3D rock illustration, and a raster.
- `omo.png` — a landscape screenshot, not a mark.
- The npm tarball carries no `.svg`, `.png`, or `.ico` at all.

## Which maps it joins

`CLIENT_MARKS.omo` — yes.

`MONOCHROME_CLIENT_MARKS` — no. The artwork is two inks: an `#F4F4F4` rounded
plate with an `#041617` face on it. Masking a plated mark discards the plate and
the face together and renders a filled square at 20px, which is the failure the
monochrome set exists to avoid rather than an instance of it.

Provenance is recorded alongside the file in
`gui/public/provider-icons/README.md`, in both the per-file source list and the
masked/not-masked ledger. That was done in wp1 rather than wp3: the asset landed
in this cycle, and an uncited file in that directory is exactly what the README
exists to prevent.
Loading
Loading