Publish the @unifia scope and cut CLI distribution loose from upstream - #24
Closed
Rwanbt wants to merge 4 commits into
Closed
Publish the @unifia scope and cut CLI distribution loose from upstream#24Rwanbt wants to merge 4 commits into
Rwanbt wants to merge 4 commits into
Conversation
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.
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
Owner
Author
|
Superseded by #25, which was squash-merged into Verified rather than assumed: |
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes # (no issue — follow-up to the distribution blockers listed in #23)
Type of change
What does this PR do?
Two things, in four commits.
1. Makes the
@unifiascope installable.Config.installDependenciesinjects@unifia/pluginpinned to the running CLI version, and npm answered 404 for the whole scope. Three reasons it could not have worked:@unifia/sdk-sharedwasprivate: truewhile the other two depend on it; all three declaredfiles: ["dist"]against anexportsmap pointing at./src/*.ts, so the tarball had no entry points; andtscis incremental here (composite), so with a staletsconfig.tsbuildinfoit exits 0 while emitting nothing —rm -rf dist && tsccould publish an empty tarball. The export rewrite moved to a shared helper that restores the manifest in afinally.2. Stops the CLI's distribution resolving to upstream. The npm update check read upstream's
opencode-aiand then installedunifia-aiat 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 upgradeon a curl install fetched and ranhttps://opencode.ai/install, replacing Unifia with the upstream product. Curl installs were detected by.opencode/binwhileinstalldeploys to.unifia/bin, so they fell through tounknownand 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.ymlwent with them — it triggered onrelease: publishedwith no repository gate, so publishing a release here opened a pull request onzed-industries/extensions.fork-release.ymlbecomesrelease.ymlonv*tags, and gains npm and container jobs reusing the binariesbuild-clialready cross-compiles. It replaces a stubrelease.ymlthat 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?
@unifiapackages were published to a local Verdaccio in dependency order, then installed into a clean project outside the workspace:npm install @unifia/plugin@1.3.15resolves the tree through npm's own installer, typechecks understrictwithskipLibCheck: falseacross every subpath, and imports at runtime. Same viabun add.npm view.packages/opencode/script/publish-npm.tsdry-run against a realscript/build.tsbuild, twice, to confirm idempotence — the second run caught the stub landing in its ownoptionalDependencies.Screenshots / recordings
Not a UI change.
Checklist