Skip to content

Publish the @unifia scope and cut CLI distribution loose from upstream - #24

Closed
Rwanbt wants to merge 4 commits into
feat/unifia-rebrand-completefrom
feat/npm-publish-unifia-scope
Closed

Publish the @unifia scope and cut CLI distribution loose from upstream#24
Rwanbt wants to merge 4 commits into
feat/unifia-rebrand-completefrom
feat/npm-publish-unifia-scope

Conversation

@Rwanbt

@Rwanbt Rwanbt commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Issue for this PR

Closes # (no issue — follow-up to the distribution blockers listed in #23)

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Two things, in four commits.

1. Makes the @unifia scope installable. Config.installDependencies injects @unifia/plugin pinned to the running CLI version, and npm answered 404 for the whole scope. Three reasons it could not have worked: @unifia/sdk-shared was private: true while the other two depend on it; all three declared files: ["dist"] against an exports map pointing at ./src/*.ts, so the tarball had no entry points; and tsc is incremental here (composite), so with a stale tsconfig.tsbuildinfo it exits 0 while emitting nothing — rm -rf dist && tsc could publish an empty tarball. The export rewrite moved to a shared helper that restores the manifest in a finally.

2. Stops the CLI's distribution resolving to upstream. The npm update check read upstream's opencode-ai and then installed unifia-ai at whatever version it returned — upstream is at 1.18.16 against this product's 1.3.x, so every npm install was offered an upgrade that does not exist. unifia upgrade on a curl install fetched and ran https://opencode.ai/install, replacing Unifia with the upstream product. Curl installs were detected by .opencode/bin while install deploys to .unifia/bin, so they fell through to unknown and had no upgrade method at all.

Homebrew, Chocolatey, Scoop and the AUR are removed rather than re-pointed: this fork publishes to none of them, and detecting a channel we do not publish to is what produced the phantom upgrade offers. sync-zed-extension.yml went with them — it triggered on release: published with no repository gate, so publishing a release here opened a pull request on zed-industries/extensions.

fork-release.yml becomes release.yml on v* tags, and gains npm and container jobs reusing the binaries build-cli already cross-compiles. It replaces a stub release.yml that fired on the same tags and published an empty draft from paths nothing produces.

Both inherited publish scripts now refuse to run without UNIFIA_ALLOW_UPSTREAM_PUBLISH; they are otherwise untouched so the monthly upstream sync stays conflict-free.

How did you verify your code works?

  • The three @unifia packages were published to a local Verdaccio in dependency order, then installed into a clean project outside the workspace: npm install @unifia/plugin@1.3.15 resolves the tree through npm's own installer, typechecks under strict with skipLibCheck: false across every subpath, and imports at runtime. Same via bun add.
  • They are now live on npm (1.3.15) and verified with npm view.
  • packages/opencode/script/publish-npm.ts dry-run against a real script/build.ts build, twice, to confirm idempotence — the second run caught the stub landing in its own optionalDependencies.
  • Both guards confirmed to abort.
  • Typecheck clean; 148/148 config and installation tests, 185/185 CLI tests. Three new installation tests assert no code path reaches upstream's registries.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

MM2-B02-WORKER added 4 commits August 10, 2026 19:52
`@unifia/plugin` is injected into the package.json of every config directory
the CLI manages, pinned to the running CLI version, so the scope has to resolve
on npm. Three things stopped any of it from being installable:

- `@unifia/sdk-shared` was `private: true`, had no build and no publish script,
  while both `@unifia/sdk` and `@unifia/plugin` depend on it.
- All three declared `files: ["dist"]` against an `exports` map pointing at
  `./src/*.ts`. The publish scripts rewrote the map in place, but left it
  rewritten on failure and emitted `import` ahead of `types`.
- `tsc` is incremental here (composite). With a stale tsconfig.tsbuildinfo it
  reports success while emitting nothing, so `rm -rf dist && tsc` could produce
  an empty tarball. The tsbuildinfo now goes with dist, via a `compile` script.

The export rewrite moves to `@unifia/script/npm`, shared by the three publish
scripts, and restores the manifest byte-for-byte in a `finally`. Adds
`script/publish-npm.ts` to publish the libraries alone, in dependency order,
with UNIFIA_NPM_DRY_RUN for a pack-only run.

Each package gains a README, a LICENSE and the repository/homepage/bugs
metadata an npm page needs. `@types/node` is declared an optional peer of the
plugin: its `$` declarations name `Buffer` and `BufferEncoding`.

Verified against a local Verdaccio: publish in order, then a clean project
outside the workspace resolves `@unifia/plugin@1.3.15` through npm's own
installer, typechecks strict with skipLibCheck off, and imports at runtime.
The fork already had its own release pipeline (fork-release.yml, gated on
Rwanbt/unifia) while upstream's publish.yml is gated on anomalyco/opencode and
cannot fire here. What was missing was npm, and what was left behind was a set
of paths still resolving to upstream.

Distribution now: GitHub Releases + the install script, the desktop updaters,
the Android APK, npm, and a container on ghcr.io/<owner>/unifia. Homebrew,
Chocolatey, Scoop, the AUR and the Zed extension are removed rather than
re-pointed — publishing to none of them, detecting them only produced upgrade
offers computed from someone else's version line.

Removed a release-triggered path that reached a third party: no repository gate
on sync-zed-extension.yml, so publishing a release here opened a pull request
on zed-industries/extensions carrying a manifest whose download URLs pointed at
anomalyco/opencode.

Upgrade path, all in installation/index.ts:
- the npm check read upstream's `opencode-ai` and then installed `unifia-ai` at
  whatever version that returned. Upstream is at 1.18.16 against this fork's
  1.3.15, so every npm install was told to upgrade to a release that does not
  exist.
- `unifia upgrade` on a curl install fetched and ran https://opencode.ai/install
  — upstream's installer, on a domain this fork does not control — replacing
  Unifia with the upstream product. It now runs the installer this repo ships as
  a release asset.
- curl installs were detected by `.opencode/bin` only, while `install` deploys
  to `.unifia/bin`, so a current install fell through to "unknown" and had no
  upgrade method at all.

packages/opencode/script/publish-npm.ts publishes the 12 platform packages and
the unifia-ai stub, reusing the binaries build-cli already cross-compiles on one
Linux runner. Both inherited publish scripts now refuse to run without
UNIFIA_ALLOW_UPSTREAM_PUBLISH, so a manual invocation cannot push to ghcr.io/
anomalyco, the AUR or upstream's Homebrew tap; they are otherwise untouched, so
the monthly upstream sync stays conflict-free.

Also: the Linux .deb registered its AppStream metadata as
ai.opencode.opencode.metainfo.xml while the file declares ai.unifia.desktop —
both an upstream identity and a spec violation, since the filename must match
the component id. Beta updater pointed at the pre-rename Rwanbt/opencode-beta.
Nix homepages and the installer's closing link pointed at opencode.ai.

Verified: typecheck clean, 7/7 installation tests (three of them new, asserting
no code path reaches upstream's registries), 185/185 CLI tests, both guards
fire, and publish-npm.ts dry-runs idempotently against a real build.
…ode-ai

A release cut without UNIFIA_VERSION read upstream's latest npm version and
bumped that. Upstream is at 1.18.16 against this product's 1.3.x, so the next
tag would have jumped an entire version line that is not ours.
Unifia released as `fork-release.yml` on `v*-fork*` tags with
UNIFIA_CHANNEL=fork. That is the naming a fork gives itself; nothing downstream
required it — Installation.isPreview(), the only reader of the channel, has no
callers. The workflow is now `release.yml` on `v*` (still matching the
`v*-fork*` tags cut before this) with the channel on `latest`, which is also
what `npm i -g unifia-ai` resolves.

Deleted the stub it collides with: a second `release.yml`, added with the
Docker scaffolding and never wired to a build, fired on `v*.*.*` with no
repository gate and published an empty draft from `dist/*` and `*.tar.gz` at
the repo root — paths nothing produces.

`opencode.yml` becomes `unifia.yml` and runs the composite action this repo
ships in github/ rather than `anomalyco/opencode/github@<sha>`: every `/unifia`
comment was executing upstream's agent at a revision this fork does not
control. Trigger words follow (`/uni`, `/unifia`). The `opencode/claude-opus-4-5`
model stays — `opencode` there is a provider id (provider/schema.ts:27), not
branding.

Cleared the `@unifia-ai` spelling left by an intermediate rebrand pass. One of
them was load-bearing: file-viewer.spec.ts asserted the file viewer shows
`"@unifia-ai/app"` while the package is `@unifia/app`, so the assertion could
never have matched.

Verified: typecheck clean, 148/148 config and installation tests.
@github-actions

Copy link
Copy Markdown

Hey! Your PR title Publish the @unifia scope and cut CLI distribution loose from upstream doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@Rwanbt

Rwanbt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #25, which was squash-merged into feat/unifia-rebrand-complete as 6cca33b.

Verified rather than assumed: git merge-base --is-ancestor origin/feat/npm-publish-unifia-scope origin/feat/unifia-c8-c9 exits 0, so every commit on this branch is contained in #25, and git diff origin/feat/unifia-c8-c9 origin/feat/unifia-rebrand-complete is empty, so the squash preserved the content exactly. Nothing from this PR is lost by closing it.

@Rwanbt Rwanbt closed this Aug 11, 2026
Rwanbt pushed a commit that referenced this pull request Aug 11, 2026
It was selected because it appeared on PRs #23, #24 and #25 — which turned
out not to prove what it looked like. `observability-sdk-drift.yml` filters
its `pull_request` trigger by `paths`, so it never started on #28, and a
required check that does not start stays "Expected — waiting for status"
forever. Requiring it would have permanently blocked every pull request that
does not touch the SDK.

Seven checks remain, each verified in its own workflow file to have an
unfiltered `pull_request` (or `pull_request_target`) trigger rather than
inferred from a sample of pull requests. The ledger records the trap so the
eighth is not added back on the same reasoning.
@Rwanbt
Rwanbt deleted the feat/npm-publish-unifia-scope branch August 11, 2026 12:53
Rwanbt added a commit that referenced this pull request Aug 11, 2026
* feat(npm): make the @UNiFia scope publishable

`@unifia/plugin` is injected into the package.json of every config directory
the CLI manages, pinned to the running CLI version, so the scope has to resolve
on npm. Three things stopped any of it from being installable:

- `@unifia/sdk-shared` was `private: true`, had no build and no publish script,
  while both `@unifia/sdk` and `@unifia/plugin` depend on it.
- All three declared `files: ["dist"]` against an `exports` map pointing at
  `./src/*.ts`. The publish scripts rewrote the map in place, but left it
  rewritten on failure and emitted `import` ahead of `types`.
- `tsc` is incremental here (composite). With a stale tsconfig.tsbuildinfo it
  reports success while emitting nothing, so `rm -rf dist && tsc` could produce
  an empty tarball. The tsbuildinfo now goes with dist, via a `compile` script.

The export rewrite moves to `@unifia/script/npm`, shared by the three publish
scripts, and restores the manifest byte-for-byte in a `finally`. Adds
`script/publish-npm.ts` to publish the libraries alone, in dependency order,
with UNIFIA_NPM_DRY_RUN for a pack-only run.

Each package gains a README, a LICENSE and the repository/homepage/bugs
metadata an npm page needs. `@types/node` is declared an optional peer of the
plugin: its `$` declarations name `Buffer` and `BufferEncoding`.

Verified against a local Verdaccio: publish in order, then a clean project
outside the workspace resolves `@unifia/plugin@1.3.15` through npm's own
installer, typechecks strict with skipLibCheck off, and imports at runtime.

* feat(release): cut the CLI's distribution loose from upstream

The fork already had its own release pipeline (fork-release.yml, gated on
Rwanbt/unifia) while upstream's publish.yml is gated on anomalyco/opencode and
cannot fire here. What was missing was npm, and what was left behind was a set
of paths still resolving to upstream.

Distribution now: GitHub Releases + the install script, the desktop updaters,
the Android APK, npm, and a container on ghcr.io/<owner>/unifia. Homebrew,
Chocolatey, Scoop, the AUR and the Zed extension are removed rather than
re-pointed — publishing to none of them, detecting them only produced upgrade
offers computed from someone else's version line.

Removed a release-triggered path that reached a third party: no repository gate
on sync-zed-extension.yml, so publishing a release here opened a pull request
on zed-industries/extensions carrying a manifest whose download URLs pointed at
anomalyco/opencode.

Upgrade path, all in installation/index.ts:
- the npm check read upstream's `opencode-ai` and then installed `unifia-ai` at
  whatever version that returned. Upstream is at 1.18.16 against this fork's
  1.3.15, so every npm install was told to upgrade to a release that does not
  exist.
- `unifia upgrade` on a curl install fetched and ran https://opencode.ai/install
  — upstream's installer, on a domain this fork does not control — replacing
  Unifia with the upstream product. It now runs the installer this repo ships as
  a release asset.
- curl installs were detected by `.opencode/bin` only, while `install` deploys
  to `.unifia/bin`, so a current install fell through to "unknown" and had no
  upgrade method at all.

packages/opencode/script/publish-npm.ts publishes the 12 platform packages and
the unifia-ai stub, reusing the binaries build-cli already cross-compiles on one
Linux runner. Both inherited publish scripts now refuse to run without
UNIFIA_ALLOW_UPSTREAM_PUBLISH, so a manual invocation cannot push to ghcr.io/
anomalyco, the AUR or upstream's Homebrew tap; they are otherwise untouched, so
the monthly upstream sync stays conflict-free.

Also: the Linux .deb registered its AppStream metadata as
ai.opencode.opencode.metainfo.xml while the file declares ai.unifia.desktop —
both an upstream identity and a spec violation, since the filename must match
the component id. Beta updater pointed at the pre-rename Rwanbt/opencode-beta.
Nix homepages and the installer's closing link pointed at opencode.ai.

Verified: typecheck clean, 7/7 installation tests (three of them new, asserting
no code path reaches upstream's registries), 185/185 CLI tests, both guards
fire, and publish-npm.ts dry-runs idempotently against a real build.

* fix(release): derive the version from unifia-ai, not upstream's opencode-ai

A release cut without UNIFIA_VERSION read upstream's latest npm version and
bumped that. Upstream is at 1.18.16 against this product's 1.3.x, so the next
tag would have jumped an entire version line that is not ours.

* refactor(release): name the product, not what it descends from

Unifia released as `fork-release.yml` on `v*-fork*` tags with
UNIFIA_CHANNEL=fork. That is the naming a fork gives itself; nothing downstream
required it — Installation.isPreview(), the only reader of the channel, has no
callers. The workflow is now `release.yml` on `v*` (still matching the
`v*-fork*` tags cut before this) with the channel on `latest`, which is also
what `npm i -g unifia-ai` resolves.

Deleted the stub it collides with: a second `release.yml`, added with the
Docker scaffolding and never wired to a build, fired on `v*.*.*` with no
repository gate and published an empty draft from `dist/*` and `*.tar.gz` at
the repo root — paths nothing produces.

`opencode.yml` becomes `unifia.yml` and runs the composite action this repo
ships in github/ rather than `anomalyco/opencode/github@<sha>`: every `/unifia`
comment was executing upstream's agent at a revision this fork does not
control. Trigger words follow (`/uni`, `/unifia`). The `opencode/claude-opus-4-5`
model stays — `opencode` there is a provider id (provider/schema.ts:27), not
branding.

Cleared the `@unifia-ai` spelling left by an intermediate rebrand pass. One of
them was load-bearing: file-viewer.spec.ts asserted the file viewer shows
`"@unifia-ai/app"` while the package is `@unifia/app`, so the assertion could
never have matched.

Verified: typecheck clean, 148/148 config and installation tests.

* feat(config): read `.opencode`, write `.unifia`

The project config directory — skills, themes, agents, commands, plugins,
learnings, plans — was `.opencode` on both the read and the write side. That is
the same directory the separately-installed OpenCode uses, so this product was
loading the other one's project configuration and, through installDependencies
on every config load, writing a package.json, a .gitignore and a node_modules
tree into it.

Reads now cover both names, writes only `.unifia`. The merge order is the rule
projectFiles() already applies to filenames — legacy first, current brand last
so it wins where both exist — extended to the directory name, which is the one
place it had never been applied. Nothing a user wrote before the rename
disappears: skills, themes, agents and commands keep loading from `.opencode`,
learnings are read from both directories and sorted together by date, and a plan
already written to the old path still resolves there rather than reading as
empty at a new one.

The global config directory needed nothing — Global.Path is XDG-based and has
been `unifia` since the rebrand.

One deliberate consequence: a tool sitting in a legacy `.opencode` directory
that declares npm dependencies no longer gets them installed, because that
directory is no longer written to. It still loads; only dependency installation
moves. Moving the directory to `.unifia` restores it.

Verified: typecheck clean, 682 tests across config, tool, session and
coexistence. Three new tests in test/config/paths.test.ts pin the discovery
order, the legacy-only case, and the read-only contract; the pre-existing
registry test that loads a tool from `.opencode/tool` now doubles as the
legacy-read regression. The single failure in the combined run
(processor-observability, 5082ms against a 5000ms limit) reproduces identically
on the base branch and passes in isolation — run contention, not this change.

* feat(storage): carte C8-A - rename opencode.db to unifia.db with one-shot copy

- TS: storage/db.ts exports DATABASE_FILE and LEGACY_DATABASE_FILE plus
  migrateLegacyDatabaseFile() that copies the legacy opencode.db (and its
  -wal / -shm siblings) to unifia.db on first access. Idempotent, never
  moves the source. getChannelPath() now returns the unifia path.
- TS: mobile-entry.ts:196 uses the new file as the JsonMigration marker.
- Rust: lib.rs sidecar_db_path() mirrors the TS migration with the same
  copy-not-move semantics. New migrate_legacy_db() and append_suffix()
  helpers. tracing::info! on copy, tracing::error! + Err on failure.
- Tests: db-migration.test.ts covers 5 cases (copy, -wal/-shm, idempotent,
  no source, both present). Rust db_migration_tests covers the 4 main
  cases (compile OK, runtime blocked by a Windows DLL env issue).
- db.test.ts expectations aligned to the new file naming.

Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-A
Gate: 5/5 - content identical, legacy preserved, idempotent, cargo check,
      tests cover all three cases (legacy present / absent / both)

* feat(keyring): carte C8-B - namespace keychain opencode.* -> unifia.*

- New crate unifia-keyring-shim owns the brand-prefixed keyring calls
  (get / set / delete) so the migration logic is unit-testable on a
  workstation without WebView2 / DirectML installed. It exposes a
  KeyringBackend trait; RealKeyringBackend delegates to the keyring
  crate, MockBackend is an in-process HashMap for tests. The keyring
  crate's own mock module cannot be used here because it does not
  persist data between Entry::new calls.
- auth_storage.rs now uses the shim and only the Tauri-side index
  management stays in the lib. The four Tauri commands keep the same
  public signature.
- Migration semantics match the runbook: get tries new first, falls
  back to legacy and rewrites on hit (legacy kept as backup); set
  writes to new only; delete removes BOTH prefixes so a logout
  cannot leave a phantom credential under the legacy namespace.
- Tests in keyring-shim/tests/keychain_namespace.rs cover 7 cases:
  rewrite on legacy hit, prefer new when both present, set writes
  to new only, delete clears both, delete is a no-op when nothing
  exists, get returns None when both empty, second read uses the
  rewritten new prefix.

Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-B
Gate: 3/3 - legacy credential becomes visible under new prefix,
      logout clears both prefixes (verified by 7 tests), cargo
      check passes

* refactor(packages): carte C9 - rename packages/opencode to packages/unifia + opencode-cli to unifia-cli

Directory rename:
- git mv packages/opencode packages/unifia (1380 files affected)

Path references: 245+31 files updated from packages/opencode to
packages/unifia, excluding the docs/autonomy/ provenance files
(UPSTREAM-*, BASELINE.md, MIGRATION-PLAN.md, REPO-INVENTORY.md)
which the runbook C9 gate explicitly preserves as historical record.

Sidecar rename: opencode-cli -> unifia-cli in producer and consumer:
- .github/workflows/{android,publish,release}.yml — CI artifact names
- packages/desktop-electron/{electron-builder.config.ts, .gitignore,
  scripts/{prepare,utils}.ts, src/main/cli.ts} — Electron packaging
- crates/unifia-supervisor/src/tests.rs — left alone: the /opt/opencode/
  opencode-cli path there is a forged-lease fixture the supervisor
  must reject, not a real install path.
- packages/unifia/src/account/index.ts — left alone: clientId is the
  OAuth client identifier registered with the provider.

The build script packages/unifia/script/build.ts already derives
the binary basename from pkg.name (= unifia) without hardcoding
the sidecar suffix; the copy-sidecar step in packages/desktop/scripts/
adds the -cli suffix when staging the artifact.

Tauri config (sidecars/unifia-cli-{triple}) was already aligned
with the new name; no change required there.

Bonus: pre-existing shellcheck warnings on test-tools.sh (SC2086 on
the basename call, SC2164 on a bare cd) were caught by the pre-commit
hook on the touched file and are fixed here to unblock the commit.

Validation:
- cargo check (packages/desktop/src-tauri): passes
- tsc --noEmit (packages/unifia): 1 pre-existing error in
  config.ts:634 (TS2589) confirmed to be present on the base
  branch before this commit; no new errors introduced
- grep -rn 'packages/opencode' (excl. node_modules): 3 files,
  all in docs/autonomy/ provenance, as required by the gate

* fix(build): repair relative path references missed by C9 rename

The C9 mass-rename caught all 'packages/opencode' absolute references
but missed the 'relative' imports and shell cd invocations that point
into the renamed directory. These broke three build steps the C9
gate did not exercise:

- packages/sdk/js/script/openapi.ts:26 — SDK build fetches
  'bun run dev generate' from the renamed CLI package.
- packages/app/e2e/fixtures.ts:4-5 and packages/app/script/e2e-local.ts:
  167-176 — e2e harness imports 'log', 'installation', 'flag',
  'server', 'instance' from the renamed CLI.
- packages/desktop-electron/scripts/utils.ts:14,118 and
  packages/desktop/scripts/{copy-sidecar,predev,utils}.ts —
  sidecar staging reads the CLI manifest, cds into the CLI directory
  for the build, and locates 'dist/' under the renamed path.
- packages/console/app/package.json:10 — Vite build runs the CLI
  schema generator at the renamed path.
- packages/mobile/scripts/prepare-android-runtime.sh:145 — Android
  bundle locates the CLI at 'mobile/../<cli>' for the runtime.

Caught by the 'unit (linux)' and 'unit (windows)' CI failures on
PR #25 — the SDK build is a transitive dep of 'bun turbo test:ci'.
The compliance-close bot had not yet acted; this is a real bug,
not a bot-driven noise. Pushing the fix re-triggers CI.

Refs: PR #25, Runbook-Autonome-Independance-Unifia-2026-08-10 C9

* fix(test): align plugin install tests with config-dir-migration write target

c1d76cf (config-dir-migration, in the lineage of feat/unifia-c8-c9)
changed packages/unifia/src/config/paths.ts to write to .unifia/ and
read from .opencode/ for backward compatibility. The
plugin.install.task tests in test/plugin/install.test.ts still read
from .opencode/, so after that commit the tests could not find the
config the install wrote — 17 ENOENT failures, caught by the
'unit (linux)' check on PR #25.

Bug was pre-existing in c1d76cf but invisible there because the
test branch (feat/unifia-rebrand-complete) does not include the
config-dir-migration commit. Surfaced only now that
feat/unifia-c8-c9 stacks on top of feat/unifia-config-dir-migration.

Aligned all 30 .opencode -> .unifia references in the test to the
new write target.

* fix(test): align remaining plugin install tests with config-dir-migration

Same root cause as the previous install.test.ts fix: c1d76cf
wrote plugin config to .unifia/ but the install-concurrency and
auth-override tests still read from .opencode/. Three concurrent
test cases were failing with ENOENT on Linux (and Windows, same
file).

The 30+ other .opencode references in test/ are intentional:
install-coexistence / uninstall-coexistence verify coexistence
with a real OpenCode install, config.test.ts exercises the legacy
.read path, agent.test.ts tests the .opencode/plans permission
glob, plugin-loader.test.ts verifies theme resolution. All
correctly preserved as-is per the c1d76cf 'legacy read' contract.

* refactor(mobile): finish Unifia runtime rename

* fix(workspace): clear rebrand follow-up debt

* fix(android): enforce release signing identity

* fix(e2e): follow renamed Unifia server package

* fix(e2e): start isolated Unifia backends

* fix(identity): read the isolated UNIFIA_* environment names

Every shell that launches the sidecar — the mobile Rust runtime
(runtime/server.rs), the Tauri desktop (cli.rs) and Electron (main/cli.ts) —
exports UNIFIA_CLIENT, UNIFIA_AUTH_STORAGE and UNIFIA_SERVER_*. These are the
`isolated` class in config/identity.json, so Flag deliberately refuses to
satisfy them from the OPENCODE_ spelling: an environment prepared for the
separately-installed OpenCode must not reach this product's keychain or
credentials.

Six consumers still read the legacy names straight from process.env, so the
value the shells emit never arrived and each silently took its default:

- github/auth.ts picked plaintext `file` storage on mobile instead of
  `encrypted-file`
- github/credentials.ts skipped the mobile git-credential passthrough
- local-llm-server/index.ts took the desktop spawn path on Android, where
  LlamaService owns llama-server
- tool/bash.ts dropped the --init-file workaround the musl bash build needs
- mobile-entry.ts *set* OPENCODE_CLIENT, which isolatedValue ignores, leaving
  Flag.UNIFIA_CLIENT reading "cli" inside the mobile sidecar
- cli/cmd/team.ts authenticated with OPENCODE_SERVER_* and defaulted the
  username to "opencode", while server/auth-jwt.ts validates UNIFIA_SERVER_*
  against "unifia" — the Team CLI could never authenticate

Also names the Team CLI surface Unifia in its help text.

* fix(config): write project config to the current brand directory

ConfigPaths.PROJECT_DIRECTORY is `.unifia` and its doc comment states that
`.opencode` is read-only by contract: the same directory belongs to the
separately-installed OpenCode, so writing there drops files into that
product's project. plugin/install.ts already respects this; three write
paths did not.

- cli/cmd/init.ts created `.opencode/unifia.jsonc` while telling the user it
  had written `.opencode/opencode.jsonc` — neither path was the real one
- cli/cmd/agent.ts created project agents under `.opencode/agent`
- local-models/index.ts wrote `.opencode/opencode.jsonc`, legacy in both the
  directory and the file name

All three now resolve through ConfigPaths.PROJECT_DIRECTORY, and init derives
its user-facing label from the same constant so the message cannot drift from
the path again.

* docs(brand): name the product Unifia in system prompts and templates

The ten system prompts introduced the agent as OpenCode and pointed users at
anomalyco/opencode and opencode.ai for feedback and documentation, so a Unifia
session told the user it was a different product and sent bug reports
upstream. Feedback now goes to github.com/Rwanbt/unifia/issues and the docs
reference is the repository, which is the only location this fork controls —
no domain is invented.

initialize.txt likewise asked the model to write AGENTS.md for "future
OpenCode sessions" and to reference `opencode.json`, which is the legacy
config name rather than the one `unifia init` now writes.

* chore(governance): make the fork own its own docs and review rules

Six inherited or mechanically-rewritten statements described a repository
this is not.

Upstream is named OpenCode, not Unifia. A global rename had rewritten the
*upstream* references too, so README and AGENTS.md called it "upstream Unifia"
and described this repo as a fork of itself. Restored in six places; the fork
notice now names the two products distinctly, which is the whole point of it.

- SECURITY.md sent vulnerability reports to anomalyco/opencode advisories.
  Reports about fork-only code (desktop/mobile shells, local model
  orchestration, Team, identity gates) reached maintainers who cannot act on
  them. Now routes to this repository, with a table for deciding which project
  owns a defect, and an escalation path sized for a single maintainer instead
  of upstream's 6-business-day organisational SLA.
- .github/CODEOWNERS assigned this fork's packages to @adamdotdevin and
  @Brendonovich, who have no access here, and routed `packages/tauri/`, which
  does not exist. Rewritten around the only account with access, so
  "require Code Owners review" becomes enforceable rather than unsatisfiable.
- README's build badge pointed at fork-release.yml, deleted when the workflow
  was renamed to release.yml, so it rendered as a permanent "no status".
- README still said "The final product name is not fixed yet" and called this
  a "working-name fork" — contradicted by config/identity.json, which the
  identity gate enforces on every surface.
- AGENTS.md claimed the default branch is `dev` and that a local `main` may not
  exist. `gh api repos/Rwanbt/unifia --jq .default_branch` returns `main`, and
  the two branches have diverged (27 / 32 commits).
- CHANGELOG linked Rwanbt/opencode, the repository's pre-rename name. It only
  resolves through a GitHub redirect; the canonical name is now used and the
  former one is stated once as history.

Also drops two placeholder root scripts (`random`, `hello`) with no callers.

* docs(readiness): make one gate ledger authoritative

Three documents claimed to describe release readiness and disagreed.
PROD_READINESS.md already called itself the single entry point but was last
assessed on 2026-04-19, so it predated the whole rebrand and named none of
its gates; PRODUCTION_READINESS.md was a never-filled checklist dated
2026-07-31 whose empty boxes could be read as "not applicable" rather than
"never verified".

PROD_READINESS.md keeps the role and gains the ledger the decision actually
needs, with each gate separated by nature — local, CI, human security, GitHub
governance, signing and device, packaging, product identity, domain, external
publication — and a stated rule for what may be promoted. Every entry cites
the run, the count or the API response it rests on; nothing is checked from a
summary. The 2026-04-19 UX verdict is kept and stated as open in addition to,
not instead of, the rebrand gates.

Also records the classification of the remaining `opencode` references, so a
future pass can tell a legitimate external contract from an unfinished rename
without re-deriving it, and the reason the branch-protection JSON is prepared
but not applied: a single-maintainer repository cannot satisfy a required
self-approval, so requiring one alongside enforce_admins would lock both
branches permanently.

PRODUCTION_READINESS.md is marked superseded rather than deleted — it remains
a usable thematic checklist, it just does not decide anything.

* docs(readiness): record the branch protection actually applied

`main` and `dev` now carry 8 required status checks, one approval,
dismiss-stale-reviews, linear history, and the force-push/deletion bans they
already had. `enforce_admins` stays false on purpose and the ledger now says
why: GitHub forbids approving your own pull request, so requiring an approval
while also binding admins would leave a single-maintainer repository with no
one able to merge anything. The same reasoning keeps require_code_owner_reviews
off even though CODEOWNERS is now correct.

The previous configuration is saved outside the repository, so the change is
reversible.

* fix(governance): drop sdk-drift from the required checks

It was selected because it appeared on PRs #23, #24 and #25 — which turned
out not to prove what it looked like. `observability-sdk-drift.yml` filters
its `pull_request` trigger by `paths`, so it never started on #28, and a
required check that does not start stays "Expected — waiting for status"
forever. Requiring it would have permanently blocked every pull request that
does not touch the SDK.

Seven checks remain, each verified in its own workflow file to have an
unfiltered `pull_request` (or `pull_request_target`) trigger rather than
inferred from a sample of pull requests. The ledger records the trap so the
eighth is not added back on the same reasoning.

* fix(brand): point fork-owned links at the repository, not a domain

Applies the domain decision of 2026-08-11: the fork controls no domain, so
every link it owns points at github.com/Rwanbt/unifia. No domain is invented;
`unifia.ai` appears only in the comment forbidding it.

Deliberately narrow. `opencode.ai` occurs 3749 times in this repository, and
roughly 3300 of those are external contracts a rename would break: 2291
`$schema` URLs that make editors validate config files, ~700 Zen API endpoints
the `opencode` provider actually calls, and ~200 theme schemas. Also left
pointing upstream are the docs site's `console`, `email` and `discord` — Zen's
console, anoma.ly's enterprise contact and upstream's Discord are their
services, described on pages documenting them, so redirecting those would
replace a working instruction with a wrong one.

What changed instead:

- packages/web/config.mjs is the single producer every one of the 21 locales
  reads for `config.github`, so `edit this page` and the SDK type links stopped
  sending contributors to anomalyco/opencode. `url` becomes overridable through
  UNIFIA_SITE_URL and otherwise names the repository; while the site is
  undeployed there is no origin to name, and the file now says so.
- astro.config.mjs called `../opencode/script/schema.ts`, a path the C9 rename
  missed. spawnSync reports a missing program through `error` rather than
  throwing and nothing read it, so the docs build kept succeeding while
  emitting no config schema at all. Path corrected and both failure modes are
  now raised.
- The CLI's own help — the TUI docs action, the comment `unifia init` writes
  into every generated config, and two config-schema descriptions users see as
  editor tooltips — pointed at upstream documentation.

config-schema.ts feeds packages/sdk, so `./script/generate.ts` was re-run and
the regenerated openapi.json and v2 types are included; committing the schema
change alone would have failed the `sdk in sync with server` gate.

Verified: packages/unifia and packages/sdk/js typecheck clean; 329 tests pass
across test/config and test/cli; biome clean on 1247 files.

* docs(readiness): correct the CodeQL gate — the alerts were not stale

The previous entry said every alert path sat under `packages/opencode/` and
concluded the scan had to be replayed before review. That reasoning was wrong
in both halves, and it understated how much of the gate is already done.

The alerts are reported against `refs/heads/dev`, and `dev` has not received
the C9 rename, so those paths are correct for the branch they describe rather
than left over from an older tree. The scan is also current: CodeQL completed
successfully on 6cca33b, which is the head of PR #23.

More usefully, both `critical` findings are already fixed on
feat/unifia-rebrand-complete and will close when it merges — cargo-proxy.mjs
now carries the ALLOWED_TOOLS allowlist its own comment claimed but never
implemented, and github-run.ts checks the prefix after URL normalisation
instead of before. What is left for a human is two high and two medium
findings. Nothing was reviewed, modified or dismissed here.

* fix(lsp): add UNIFIA_DISABLE_LSP and use it in the e2e runner

`UNIFIA_DISABLE_LSP_DOWNLOAD`, which the e2e runner already set, only stops
*fetching* a language server that is missing. The servers the runner image
already carries still start, and rust, typescript and julials each spend the
full 45 s `initialize` timeout before giving up, concurrently with Playwright's
own timers. The suite exercises no LSP feature, so it was paying that for
nothing.

Config `lsp: false` already expressed exactly this, so the new flag routes to
the same branch rather than adding a second mechanism; it exists for callers
with no config file to edit, which is the runner's situation. Fork-owned, so
no OPENCODE_ spelling is accepted. A test pins that, alongside the one that
matters most: DISABLE_LSP and DISABLE_LSP_DOWNLOAD stay independent in both
directions, since conflating them is what hid this.

What this measurably does: the runner logs `reason=UNIFIA_DISABLE_LSP all LSPs
are disabled`, with zero `spawned lsp server` and zero 45 s timeouts against
four before, and the run is shorter.

What it does NOT do, stated because the first draft of this claimed otherwise:
it does not stabilise the suite. Three local runs of the same two tests gave
LSP on -> pass (43.7 s), LSP off -> fail, LSP off -> pass (37.3 s). The tests
flake independently of the language servers, so the LSP timeouts were a real
cost but not the cause. The remaining cause is not identified, and this commit
should not be read as closing the e2e gate.

Verified: packages/unifia typecheck clean, 10 flag tests pass, packages/app
typecheck clean.

---------

Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant