Skip to content

refactor: move Global/log/flag/cross-spawn/npm into @opencode-ai/core#266

Merged
Astro-Han merged 7 commits into
devfrom
claude/upstream-sync-209-pr2-moves
Apr 27, 2026
Merged

refactor: move Global/log/flag/cross-spawn/npm into @opencode-ai/core#266
Astro-Han merged 7 commits into
devfrom
claude/upstream-sync-209-pr2-moves

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

Pulls the upstream "core package consolidation" follow-ups to PR #265 (rename), plus the npm-config snapshot from upstream HEAD:

  • 1a734adb4d core: consolidate shared infrastructure into core
  • 705f792e87 core: move Global module to @opencode-ai/core
  • 3eee2f6afa core: move cross-spawn-spawner from opencode to core
  • 1e98167b0e core: move cross-spawn-spawner to root + remove unused types
  • f5dce6d960 core: move npm service to core
  • Stage C snapshot: packages/core/src/npm.ts, npm-config.ts, fixtures, tests pulled from upstream HEAD 17701628bd

After this PR, packages/core owns: Global, Runtime, log, flag, cross-spawn-spawner, filesystem, util/*, npm, npm-config, installation/version, effect/{logger, memo-map, observability, runtime}.

Why

Stage C of the foundation sync (#209). Once core owns these modules, the rest of the upstream backlog (Effect Schema migration in Stage D, tool framework in Stage E) can land cleanly. The accidental "Global module hardcoded to opencode" regression that PawWork caught here is fixed by the carve-out below.

PawWork carve-outs in this PR

  • packages/core/src/global.ts calls Runtime.appName() instead of hardcoded "opencode". Preserves the separate ~/.config/pawwork/ data dir per feedback_pawwork_positioning.md. Will need to be reapplied each weekly sync (added to permanent carve-out list).
  • packages/opencode/src/global/index.ts kept as PawWork's local Runtime-aware Global module. Upstream's auto-rewritten @opencode-ai/core/global imports flow through to the carve-out.
  • packages/core/src/effect/logger.ts keeps PawWork's namespace-style Log API (Log.create / Log.Default / Log.init) instead of upstream HEAD's flat-export style. PawWork callers already use the namespace shape.

Mechanical cleanup

  • packages/opencode/src/installation/meta.ts removed; callers use @opencode-ai/core/installation/version (InstallationVersion / InstallationChannel).
  • ~120 PawWork-side import rewrites (../util/log@opencode-ai/core/util/log, ../flag/flag@opencode-ai/core/flag/flag, @/effect/cross-spawn-spawner@opencode-ai/core/cross-spawn-spawner, etc.).
  • @effect/opentelemetry: 4.0.0-beta.46 added to root catalog (matches the rest of the effect ecosystem).

Related Issue

#209

How To Verify

bun install
bun run --cwd packages/core typecheck
bun run --cwd packages/opencode typecheck
bun run --cwd packages/util typecheck
bun run --cwd packages/ui typecheck
bun run --cwd packages/desktop-electron typecheck

All 5 packages typecheck clean locally on this branch.

Stacked on

This PR's base is claude/upstream-sync-209-pr1-rename (#265). After #265 merges, GitHub will auto-rebase this PR onto dev.

Checklist

@Astro-Han Astro-Han added P3 Low priority upstream Tracked upstream or vendor behavior harness Model harness, prompts, tool descriptions, and session mechanics labels Apr 27, 2026
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 164 files, which is 14 over the limit of 150.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54f3b39e-3955-470a-9dc1-c5db43ca45d1

📥 Commits

Reviewing files that changed from the base of the PR and between aed4559 and 5e335cb.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (164)
  • .github/workflows/ci.yml
  • package.json
  • packages/app/script/e2e-local.ts
  • packages/app/src/context/global-sync.tsx
  • packages/core/package.json
  • packages/core/src/cross-spawn-spawner.ts
  • packages/core/src/effect/logger.ts
  • packages/core/src/effect/memo-map.ts
  • packages/core/src/effect/observability.ts
  • packages/core/src/effect/runtime.ts
  • packages/core/src/flag/flag.ts
  • packages/core/src/global.ts
  • packages/core/src/installation/version.ts
  • packages/core/src/npm-config.ts
  • packages/core/src/npm.ts
  • packages/core/src/types.d.ts
  • packages/core/src/util/log.ts
  • packages/core/src/util/opencode-process.ts
  • packages/core/test/effect/cross-spawn-spawner.test.ts
  • packages/core/test/effect/observability.test.ts
  • packages/core/test/fixture/tmpdir.ts
  • packages/core/test/npm-config.test.ts
  • packages/core/test/npm.test.ts
  • packages/opencode/src/acp/agent.ts
  • packages/opencode/src/acp/session.ts
  • packages/opencode/src/bus/index.ts
  • packages/opencode/src/cli/cmd/agent.ts
  • packages/opencode/src/cli/cmd/debug/index.ts
  • packages/opencode/src/cli/cmd/debug/scrap.ts
  • packages/opencode/src/cli/cmd/mcp.ts
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/cli/cmd/serve.ts
  • packages/opencode/src/cli/cmd/session.ts
  • packages/opencode/src/cli/cmd/web.ts
  • packages/opencode/src/cli/heap.ts
  • packages/opencode/src/cli/upgrade.ts
  • packages/opencode/src/command/index.ts
  • packages/opencode/src/config/config.ts
  • packages/opencode/src/config/migrate-default-agent.ts
  • packages/opencode/src/config/paths.ts
  • packages/opencode/src/control-plane/workspace.ts
  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/src/effect/bootstrap-runtime.ts
  • packages/opencode/src/effect/index.ts
  • packages/opencode/src/effect/instance-state.ts
  • packages/opencode/src/effect/observability.ts
  • packages/opencode/src/effect/oltp.ts
  • packages/opencode/src/effect/run-service.ts
  • packages/opencode/src/file/index.ts
  • packages/opencode/src/file/ripgrep.ts
  • packages/opencode/src/file/watcher.ts
  • packages/opencode/src/format/formatter.ts
  • packages/opencode/src/format/index.ts
  • packages/opencode/src/git/index.ts
  • packages/opencode/src/global/index.ts
  • packages/opencode/src/ide/index.ts
  • packages/opencode/src/index.ts
  • packages/opencode/src/installation/index.ts
  • packages/opencode/src/installation/meta.ts
  • packages/opencode/src/lsp/client.ts
  • packages/opencode/src/lsp/index.ts
  • packages/opencode/src/lsp/server.ts
  • packages/opencode/src/mcp/auth.ts
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/src/mcp/oauth-callback.ts
  • packages/opencode/src/mcp/oauth-provider.ts
  • packages/opencode/src/node.ts
  • packages/opencode/src/npm/index.ts
  • packages/opencode/src/patch/index.ts
  • packages/opencode/src/permission/index.ts
  • packages/opencode/src/plugin/codex.ts
  • packages/opencode/src/plugin/github-copilot/copilot.ts
  • packages/opencode/src/plugin/index.ts
  • packages/opencode/src/plugin/meta.ts
  • packages/opencode/src/plugin/shared.ts
  • packages/opencode/src/project/bootstrap.ts
  • packages/opencode/src/project/instance.ts
  • packages/opencode/src/project/project.ts
  • packages/opencode/src/project/state.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/provider/models.ts
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/src/pty/index.ts
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/control/index.ts
  • packages/opencode/src/server/fence.ts
  • packages/opencode/src/server/instance/config.ts
  • packages/opencode/src/server/instance/event.ts
  • packages/opencode/src/server/instance/global.ts
  • packages/opencode/src/server/instance/provider.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/server/middleware.ts
  • packages/opencode/src/server/proxy.ts
  • packages/opencode/src/server/ui/index.ts
  • packages/opencode/src/session/compaction.ts
  • packages/opencode/src/session/instruction.ts
  • packages/opencode/src/session/llm.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/projectors.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/revert.ts
  • packages/opencode/src/session/session.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/shell/shell.ts
  • packages/opencode/src/skill/discovery.ts
  • packages/opencode/src/skill/index.ts
  • packages/opencode/src/snapshot/index.ts
  • packages/opencode/src/storage/db.ts
  • packages/opencode/src/storage/json-migration.ts
  • packages/opencode/src/storage/storage.ts
  • packages/opencode/src/sync/index.ts
  • packages/opencode/src/tool/bash.ts
  • packages/opencode/src/tool/external-directory.ts
  • packages/opencode/src/tool/registry.ts
  • packages/opencode/src/tool/truncation-dir.ts
  • packages/opencode/src/util/index.ts
  • packages/opencode/src/util/which.ts
  • packages/opencode/src/worktree/index.ts
  • packages/opencode/test/cli/session.test.ts
  • packages/opencode/test/config/config.test.ts
  • packages/opencode/test/config/pawwork-global-config.test.ts
  • packages/opencode/test/format/format.test.ts
  • packages/opencode/test/github/ci-workflow.test.ts
  • packages/opencode/test/lsp/client.test.ts
  • packages/opencode/test/lsp/lspEnabled.test.ts
  • packages/opencode/test/lsp/server.test.ts
  • packages/opencode/test/npm.test.ts
  • packages/opencode/test/plugin/loader-shared.test.ts
  • packages/opencode/test/plugin/workspace-adaptor.test.ts
  • packages/opencode/test/preload.ts
  • packages/opencode/test/project/migrate-global.test.ts
  • packages/opencode/test/project/project.test.ts
  • packages/opencode/test/provider/gitlab-duo.test.ts
  • packages/opencode/test/provider/provider.test.ts
  • packages/opencode/test/server/cors-middleware.test.ts
  • packages/opencode/test/server/default-directory.test.ts
  • packages/opencode/test/server/session-runtime-routes.test.ts
  • packages/opencode/test/server/workspace-router.test.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/instruction.test.ts
  • packages/opencode/test/session/messages-pagination.test.ts
  • packages/opencode/test/session/processor-effect.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/revert-compact.test.ts
  • packages/opencode/test/session/session.test.ts
  • packages/opencode/test/session/snapshot-tool-race.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/storage/storage.test.ts
  • packages/opencode/test/sync/index.test.ts
  • packages/opencode/test/tool/agent.test.ts
  • packages/opencode/test/tool/bash.test.ts
  • packages/opencode/test/tool/edit.test.ts
  • packages/opencode/test/tool/grep.test.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/tool/read.test.ts
  • packages/opencode/test/tool/registry.test.ts
  • packages/opencode/test/tool/trash.test.ts
  • packages/opencode/test/tool/write.test.ts
  • packages/opencode/test/util/log.test.ts
  • packages/sdk/openapi.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/upstream-sync-209-pr2-moves

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the codebase by migrating core functionality to the @opencode-ai/core package, introducing new services for OpenTelemetry observability and npm package management using @npmcli/arborist. The changes include centralized path handling, process metadata utilities, and a significant update to dependency management. Feedback identifies several issues: top-level filesystem side effects in the global module, incorrect package resolution logic for Node.js, and fragile parsing of OpenTelemetry resource attributes. Furthermore, the review suggests avoiding silent error suppression in npm configuration loading and removing unexpected side effects, like lock file deletion, from the package lookup utility.

Comment thread packages/core/src/global.ts
Comment thread packages/core/src/npm.ts
Comment thread packages/core/src/npm-config.ts
Comment thread packages/core/src/effect/observability.ts
Comment thread packages/core/src/npm.ts
@Astro-Han Astro-Han force-pushed the claude/upstream-sync-209-pr1-rename branch from 0c6252d to 002807a Compare April 27, 2026 15:09
@Astro-Han Astro-Han changed the base branch from claude/upstream-sync-209-pr1-rename to dev April 27, 2026 15:19
@Astro-Han Astro-Han force-pushed the claude/upstream-sync-209-pr2-moves branch from 826a5f7 to def66e3 Compare April 27, 2026 15:19
@Astro-Han

Copy link
Copy Markdown
Owner Author

Rebased onto dev after #265 merged. The 5 review findings (top-level fs.mkdir in global.ts, import.meta.resolve in npm.ts, silent error swallow in npm-config.ts, OTEL_RESOURCE_ATTRIBUTES parsing in observability.ts, package-lock.json deletion in npm.ts:222) are all pre-existing upstream behaviors that this PR moves verbatim from packages/opencode (or pulls from upstream HEAD 17701628bd for the npm snapshot). The only intentional PawWork carve-out in packages/core here is global.ts:9 switching the hardcoded "opencode" to Runtime.appName() — see PR description "PawWork carve-outs in this PR".

Per AGENTS.md ("commit small / one reversible intent"), behavior changes don't belong in a mechanical move PR. If any of these prove worth fixing for PawWork, they should be tracked as separate scoped issues (e.g. lazy Path.ensure() instead of import-time mkdir; which no longer deleting package-lock.json). Resolving the threads as out-of-scope here.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #266 Review — Core consolidation follow-ups

Reviewed the full diff (159 files, +975 / -574) focusing on behavioral regressions, boundary conditions, and PawWork carve-out integrity. Findings below, sorted P0→P3.


P0 — Must fix before merge

1. global.ts top-level side effects on import — xdg ! non-null assertions can silently produce "undefined/pawwork" paths

packages/core/src/global.ts evaluates xdgData!, xdgCache!, xdgConfig!, xdgState! at module top level. On Linux systems or containers where these XDG env vars are unset, xdgData etc. are undefined, and path.join(undefined!, "pawwork") produces "undefined/pawwork" — silently, with no error. The old code had the same ! assertions but they ran inside an Effect Layer where a failure would propagate. Now the bad path is baked into Path.data etc. at import time with no recovery. Both the core global.ts and the opencode local global/index.ts have this issue.

The top-level await Promise.all([fs.mkdir(...)]) and Flock.setGlobal() also execute on every import. If any consumer imports Log from @opencode-ai/core/util/log (which chains to ../global), directory creation and flock wiring happen as a side effect of that import. This is a contract change from the old code where global.ts was side-effect-free at import time.

2. Npm.add() skips loadVirtual() — corrupted package dirs treated as valid

Old packages/opencode/src/npm/index.ts calls arborist.loadVirtual() first to verify tree integrity; only if that fails or the package is absent does it call reify(). This means a partially-installed or corrupted node_modules gets repaired on the next add().

New packages/core/src/npm.ts replaces this with afs.existsSafe(dir) — if the directory exists, it returns the cached entry point immediately without verifying the tree. A corrupted install (e.g. interrupted reify(), deleted node_modules inner files) will be treated as valid, and LSP servers / plugins will fail at spawn time with no self-repair path.


P1 — Should fix before merge (user-facing regression or high risk)

3. emitLspInstallFailedIfApplicable() removed — LSP install failure UI notification is dead

The old npm/index.ts published LSP.Event.InstallFailed to the Bus when an LSP server package failed to install. The packages/opencode/src/lsp/index.ts still defines Event.InstallFailed and still checks err instanceof Npm.InstallFailedError, but the new core npm.ts never publishes this event — it can't, since core has no knowledge of LSP or Bus. The SDK type EventLspServerInstallFailed still exists. The result: LSP install failures will be caught by the cooldown logic in lsp/index.ts, but no InstallFailed Bus event is ever fired, so any UI that renders a toast or notification for this event is dead code.

4. InstallFailedError schema changed — { pkg: string }{ add?: string[], dir: string, cause?: Defect }

packages/opencode/src/lsp/index.ts line checks err instanceof Npm.InstallFailedError — this will still match because the class name is the same. However, any code that reads err.payload.pkg (or err.pkg) will get undefined instead of the package name. The LSP error handler doesn't read the payload today, but the Bus event schema z.object({ pkg: z.string(), error: z.string() }) expects pkg which no longer exists on the error object. If anyone wires up the event again, the Zod validation will fail.

5. serviceName: "opencode" hardcoded in observability.ts — PawWork traces report as "opencode"

packages/core/src/effect/observability.ts resource() returns serviceName: "opencode". PawWork's OTel traces and logs will be attributed to the "opencode" service. This should use Runtime.appName() like the Global carve-out does, so PawWork reports as "pawwork". If this is intentional for upstream compatibility it should be documented in the carve-out list.

6. Dual Global module — cache version check TOCTOU race

Both packages/core/src/global.ts and packages/opencode/src/global/index.ts independently run the cache version check (read version → if mismatch, readdir + rm all cache contents → write new version). When both modules are loaded in the same process (which happens — core is imported by e.g. @opencode-ai/core/util/log, and opencode's local global/index.ts is imported by ~12 still-unmigrated files), these two checks race. If the cache version is being bumped, one module could delete files the other is reading, or write the version file before the other finishes cleaning. Low frequency (only fires on version bumps) but can corrupt cache state.


P2 — Should fix soon (maintainability / correctness)

7. Windows Bun workaround removed — __FAKE_PLATFORM__ = "linux" gone

Old npm/index.ts sets process.env.__FAKE_PLATFORM__ = "linux" before importing Arborist on Windows, because "Bun on Windows does not support the UV_FS_O_FILEMAP flag used by tar." New npm.ts does a plain import("@npmcli/arborist") without this workaround. If PawWork supports Windows + Bun (the project is a macOS + Windows desktop product per AGENTS.md), npm package installation may fail on Windows.

8. NpmConfig.load() silently swallows all errors → returns {}

packages/core/src/npm-config.ts catches all exceptions in load() and returns an empty object. This means authentication errors (bad .npmrc tokens), misconfigured registries, or corrupt config files are silently ignored. The caller proceeds with default registry + no auth, producing confusing "401 Unauthorized" errors downstream instead of an upfront clear failure.

9. Npm.outdated() removed with no replacement

The old Npm.outdated(pkg, cachedVersion) function checked the npm registry for newer versions. While no current code calls it, it was a public export of the Npm namespace. Any future code that needs version checking will need to re-implement this.

10. Inconsistent import styles for CrossSpawnSpawner and EffectLogger

Some files use import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner" while others use import { CrossSpawnSpawner } from "...". Both produce the same runtime result because the module uses export * as CrossSpawnSpawner from "./cross-spawn-spawner", but the inconsistency is a maintenance hazard. Same for EffectLoggerimport * as EffectLogger vs import { EffectLogger }. Recommend standardizing on { CrossSpawnSpawner } / { EffectLogger } since those are the explicit named namespace exports.

11. install() writable check silently masks permission errors

New npm.ts install() checks canWrite and returns early if false. Old code attempted the operation regardless. If a directory exists but is read-only (common in CI, Docker, or restricted environments), the install silently does nothing instead of surfacing the permission problem. The caller gets no error and no indication that dependencies weren't installed.


P3 — Nice to have / low risk

12. types.d.ts removal drops Bun global type declaration

packages/core/src/types.d.ts had declare var Bun: ... | undefined. If @types/bun isn't configured for the core package, other files referencing the Bun global may get implicit any. The new npm.ts uses typeof Bun !== "undefined" which works at runtime, but type safety is reduced.

13. Npm.which() behavioral change — returns undefined instead of falling back to first binary

Old which() with no bin parameter: if the requested binary isn't found, falls through to add() and returns whatever pick() resolves. New which() with bin parameter: if the specific bin isn't in .bin/, returns Option.none(). This is actually better behavior (explicit failure vs silent fallback), but is a behavioral change that could surprise callers.

14. Half-migrated Global imports — 12 opencode source files still import from local ../global

Files like src/storage/db.ts, src/snapshot/index.ts, src/lsp/server.ts, etc. still import Global from the local opencode module rather than @opencode-ai/core/global. This means both Global modules are loaded in every process, persisting the dual-module risks (#6, #10). Completing the migration and deleting the local global/index.ts (or making it re-export from core) would eliminate the entire class of dual-Global issues.

15. mergeWithExisting: false in observability logs is a behavioral change from old loggerMergeWithExisting: true

Old packages/opencode/src/effect/observability.ts used loggerMergeWithExisting: true. New code uses mergeWithExisting: false. The new code explicitly composes EffectLogger.logger + OtlpLogger.make() so both are present, but if any downstream code added custom loggers to the Effect runtime independently, they'd be dropped when OTel is enabled. The non-OTel path is unaffected.

16. setGlobalContextManager is a global singleton mutation in traces()

packages/core/src/effect/observability.ts calls context.setGlobalContextManager(mgr). This cannot be undone and will throw or silently overwrite if called more than once. In test environments that import this module multiple times or across test isolation boundaries, this could cause failures.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #266 Review — Supplemental findings (round 2)

Deeper inspection of barrel files, dead code, cross-module dependency chains, and boundary conditions missed in the first pass.


P1 — Should fix before merge

17. resource() hardcodes ensureProcessMetadata("main") — worker processes get wrong OTel process role

packages/core/src/effect/observability.ts:281 calls ensureProcessMetadata("main") every time resource() is evaluated. If a worker process sets OPENCODE_PROCESS_ROLE = "worker" via ensureProcessRole("worker") BEFORE the observability layer is built for the first time, the ??= operator will leave "worker" intact and the role is correct. But if the observability layer is built first (e.g., during app startup), ??= will set the env var to "main" permanently, and the worker's OTel data will be incorrectly labeled as opencode.process_role: main.

The fix: either don't call ensureProcessMetadata in resource() (use the env var directly with a fallback), or pass a role parameter through the layer construction chain.


P2 — Should fix soon

18. packages/opencode/src/effect/oltp.ts is dead code — duplicate Observability namespace ghost

After packages/opencode/src/effect/observability.ts was deleted (replaced by core), the sibling file oltp.ts still exists in opencode with its own export namespace Observability. No code imports from it anymore — both the barrel (packages/opencode/src/effect/index.ts) and run-service.ts now import from @opencode-ai/core/effect/observability. If any future code accidentally imports Observability from @/effect/oltp, it would get a stale, different implementation with no trace support and the old loggerMergeWithExisting: true behavior.

Recommendation: delete packages/opencode/src/effect/oltp.ts.

19. sanitize() is a no-op on non-Windows — no path traversal guard for .. in package names

packages/core/src/npm.ts:sanitize() returns the input unchanged on macOS/Linux. The directory() function does path.join(global.cache, "packages", sanitize(pkg)). On non-Windows, a crafted package name containing path traversal sequences (e.g., ../../etc passed as the pkg argument) would escape the cache directory. While npa() and Arborist validate package names against npm registry rules, the internal directory() function doesn't enforce this. A second defense-in-depth guard (e.g., path.basename(pkg) === pkg check) would be safer.

20. packages/core/src/util/log.ts triggers core global.ts top-level side effects on import

The core log.ts does import * as Global from "../global" at module scope. Since core/global.ts has top-level await (directory creation + cache version check), importing Log from @opencode-ai/core/util/log now creates directories as a side effect. This is a contract change from the old packages/opencode/src/util/log.ts which imported Global from the old opencode global that also had top-level directory creation — so this was already a side-effecting import. However, the new import chain is longer (Log → core/global → Flock → Hash, including the Flock.setGlobal() call), and the xdg ! non-null assertions in core/global can now fail silently if xdgData is undefined.

21. packages/opencode/src/effect/index.ts barrel and oltp.ts export conflicting Observability namespaces

// barrel: packages/opencode/src/effect/index.ts
export { Observability } from "@opencode-ai/core/effect/observability"  // has traces + new setup

// dead file: packages/opencode/src/effect/oltp.ts
export namespace Observability { ... }  // old Otlp.layerJson, no traces

TypeScript won't complain about this since nothing imports from oltp.ts. But if the barrel is the only import path, it'll always resolve to core. Risk: a search-and-replace or IDE auto-import could inadvertently wire up the dead oltp.ts Observability.


P3 — Nice to have

22. loggerExportInterval: Duration.seconds(1) removed — log batching behavior may change

The old packages/opencode/src/effect/observability.ts used Otlp.layerJson({ loggerExportInterval: Duration.seconds(1), ... }). The new core observability.ts uses OtlpLogger.make({ url, resource, headers }) without an explicit export interval. The default interval for OtlpLogger may differ (likely 5s or 30s depending on the Effect version). This could change OTel log delivery latency for PawWork telemetry.

23. resource() not memoized — called every time logs() is invoked

logs() creates a new Logger.layer with OtlpLogger.make({ url, resource: resource(), headers }). Each call to resource() re-computes the attributes object (including ensureProcessMetadata, OTEL_RESOURCE_ATTRIBUTES parsing, and the spread). While ensureProcessMetadata is idempotent (??=), the OTEL_RESOURCE_ATTRIBUTES parsing and object creation happen fresh each time. This is low-impact (happens once per runtime construction) but needless work.

24. packages/core/package.json has "bin": { "opencode": "./bin/opencode" } — wrong for a library package

The core package exports "bin": { "opencode": "./bin/opencode" }. Since @opencode-ai/core is a dependency library (not a directly-installed binary), this bin entry has no effect unless someone npm install -g @opencode-ai/core or another package hoists it. Harmless but misleading — if this bin entry is intended for the opencode package, it should be in packages/opencode/package.json instead.

25. ensureProcessMetadata("main") hardcoded in observability.ts but opencode-process.ts API expects a fallback parameter

ensureProcessMetadata(fallback) accepts "main" | "worker". The observability code hardcodes "main". If the observability layer is used in a worker process where OPENCODE_PROCESS_ROLE is already "worker", the ??= operator preserves the existing value and the hardcoded "main" is just a fallback. However, the fallback parameter name implies it should match the caller's context — if a worker builds the layer, the semantic fallback should be "worker", not "main". Currently this is fine because the observability layer is only built in the main process, but it's fragile documentation.

26. @effect/opentelemetry catalog entry added to root but @opentelemetry/context-async-hooks is a direct dependency in core/package.json

The root package.json adds "@effect/opentelemetry": "4.0.0-beta.46" to the catalog. The core package.json lists @opentelemetry/context-async-hooks: 2.6.1 as a direct dependency (not catalog). This is fine since it's a transitive peer, but if this dependency is moved to the catalog in the future, the version pin needs to stay synchronized.

27. Self-referential export * as Global from "./global" in core/global.ts could confuse TypeScript autocompletion

The pattern creates Global.Global.Global... in IntelliSense. For developers working in the core package, autocomplete will show the recursive self-reference as a valid path. Purely cosmetic but worth standardizing — either use export namespace Global { ... } (old style) or rename the re-export label.

Comment thread packages/core/src/effect/observability.ts
Comment thread packages/core/src/effect/observability.ts
Comment thread packages/core/src/npm.ts
Comment thread packages/core/src/npm.ts
Comment thread packages/core/src/effect/observability.ts
Comment thread packages/core/src/npm.ts Outdated
Comment thread packages/core/src/effect/observability.ts
Comment thread packages/core/src/npm.ts
Comment thread packages/core/src/npm-config.ts
Comment thread packages/core/src/npm.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #266 Inline Review — P0–P3 summary

10 inline comments posted across 3 files covering critical issues. Full summary below.

Inline comments posted

packages/core/src/effect/observability.ts (6 comments)

  • L26: P1 ensureProcessMetadata("main") hardcoded — worker process role mislabel
  • L44: P1 serviceName: "opencode" hardcoded — PawWork traces misattributed
  • L61: P2 loggerExportInterval removed — log batching behavior change
  • L67: P2 mergeWithExisting: false — behavioral change from old code

packages/core/src/npm.ts (6 comments)

  • L14: P1 InstallFailedError schema change breaks Bus event validation
  • L43: P2 sanitize() no-op on non-Windows — no path traversal guard
  • L82: P2 __FAKE_PLATFORM__ workaround removed — Windows+Bun may fail
  • L117: P2 loadVirtual() removed — corrupted packages not self-repaired
  • L139: P2 canWrite early return silently masks permission errors

packages/core/src/npm-config.ts (1 comment)

  • L13: P2 NpmConfig.load() silently swallows all errors

Additional findings not covered by inline comments

  • P0: global.ts xdg ! non-null assertions produce "undefined/pawwork" paths when XDG vars are unset
  • P0: Top-level await in global.ts triggers FS side effects on import — changes module border contract
  • P1: emitLspInstallFailedIfApplicable() removed — LSP install failure UI notification is dead code
  • P1: Dual Global module cache version check TOCTOU race when both core and opencode globals load
  • P2: packages/opencode/src/effect/oltp.ts is dead code with conflicting Observability namespace
  • P3: Inconsistent * as vs { } import styles for CrossSpawnSpawner/EffectLogger
  • P3: Core package.json carries "bin": { "opencode": ... } — wrong package for a binary

Quick action items

Priority Action
P0 Guard xdgData etc. with fallback before ! assertion in global.ts
P0 Restore loadVirtual() tree integrity check in Npm.add()
P1 Patch serviceNameRuntime.appName() for PawWork
P1 Either restore LSP install failed event or document the removal
P1 Fix InstallFailedError schema alignment with LSP Bus event
P2 Restore __FAKE_PLATFORM__ on Windows if Bun is supported
P2 Delete oltp.ts dead code

Comment thread packages/core/src/global.ts
Comment thread packages/core/src/npm.ts Outdated
@Astro-Han

Copy link
Copy Markdown
Owner Author

Disposition of GLM-5.1 review (rounds 1 + 2)

Pushed three follow-up commits to the branch:

  • 4181b92a fix(npm): preserve PawWork carve-outs in core npm move — restores arborist.loadVirtual() integrity check in Npm.add() and the Windows __FAKE_PLATFORM__ = "linux" workaround that the upstream HEAD npm.ts snapshot dropped.
  • 221911060 fix(lsp): re-emit InstallFailed Bus event around core Npm calls — wraps Npm.add with an opencode-side npmWhich helper that publishes LSP.Event.InstallFailed before falling back to undefined. Updates the Bus event payload, openapi snapshot, SDK type, and toast listener to the new {add, dir, error} shape.
  • 22140c174 chore(core): drop dead remnants from the npm move — deletes packages/opencode/src/effect/oltp.ts (stale Observability namespace), removes the bin: ./bin/opencode entry from packages/core/package.json (file doesn't exist; old shared/package.json had no bin), and drops the now-unused npm-package-arg dependency from core.

Items addressed (6/27)

# Status
#2 / #PR-line-117 / #PR-line-124 — loadVirtual() integrity check Fixed in 4181b92a
#3 / #4 — LSP InstallFailed dead listener + schema drift Fixed in 221911060
#7 — Windows __FAKE_PLATFORM__ workaround Fixed in 4181b92a
#18 / #21oltp.ts dead code with conflicting Observability namespace Fixed in 22140c174
#24 — wrong bin entry in core/package.json Fixed in 22140c174

Items pushed back as factually incorrect

Items pushed back as out-of-scope mechanical move

These reflect upstream HEAD behaviors that ship verbatim with the move. They may warrant their own focused PRs but don't belong in a "move modules into core" commit.

Verification

bun turbo typecheck   # 8 packages, all pass
bun --cwd packages/core test test/npm.test.ts            # 3 pass
bun --cwd packages/opencode test test/lsp/server.test.ts # 2 pass

CI will revalidate the full matrix.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments focused on current CI blocking regressions and PawWork boundary behavior.

Comment thread packages/core/package.json
Comment thread packages/core/src/util/log.ts
Comment thread packages/core/src/effect/observability.ts Outdated
@Astro-Han Astro-Han force-pushed the claude/upstream-sync-209-pr2-moves branch from d0fbcf3 to 9386bd8 Compare April 27, 2026 17:36

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR moves Global, log, flag, cross-spawn-spawner, npm, and observability into @opencode-ai/core. The mechanical import rewrites look correct, and the carve-outs for PawWork branding are well-documented. Below are inline-style comments organized by severity.


[P0] packages/core/src/global.ts — top-level await in module scope

The new global.ts performs top-level await Promise.all([fs.mkdir(...)]) and calls Flock.setGlobal({ state }) at import time. This is a significant behavior change from the previous Layer.effect-based lazy initialization.

  • Boundary case: In test environments where OPENCODE_TEST_HOME is set after the module is first imported (e.g., via vitest/bun test preload patterns), the paths will already be computed against the real home directory. The previous Layer.effect deferred this until Effect.runSync or similar.
  • Regression risk: Any test that imports @opencode-ai/core/global transitively before setting OPENCODE_TEST_HOME will pollute the real ~/.config/pawwork/ or ~/.cache/pawwork/ directories.
  • Suggested fix: Move the eager mkdir and Flock.setGlobal into a Global.init() function that callers invoke explicitly, or keep them inside the Layer.effect so initialization is lazy and test-controllable.

[P0] packages/core/src/npm.tsNpm.which swallows all errors silently

export async function which(...args) {
  const resolved = await runPromise((svc) => svc.which(...args))
  return Option.getOrUndefined(resolved)
}

The upstream which() implementation returns Option.none() on any failure (including InstallFailedError, EffectFlock.LockError, file system errors, etc.). The PR description acknowledges this and adds npmWhich() in lsp/server.ts to re-emit InstallFailedError.

  • Boundary case: Non-LSP callers of Npm.which (e.g., Formatter in packages/opencode/src/format/formatter.ts, Provider in provider/provider.ts) now lose all failure signals. Previously, Npm.which in the old packages/opencode/src/npm/index.ts would throw on add() failure, letting callers handle it.
  • Regression risk: Formatter or provider resolution may silently fail without any log or user feedback. The npmWhich wrapper only covers LSP servers.
  • Suggested fix: Either restore error propagation in Npm.which, or audit all non-LSP callers to ensure silent failure is acceptable.

[P1] packages/core/src/npm.tsloadArborist mutates process.env globally

if (process.platform === "win32") {
  process.env.__FAKE_PLATFORM__ = "linux"
}

This is set at module load time inside loadArborist(), which is called lazily. However, once set, it persists for the entire process.

  • Boundary case: If PawWork later needs to run actual tar operations on Windows (e.g., packaging, extracting user archives), __FAKE_PLATFORM__=linux may cause incorrect behavior in those tar instances.
  • Regression risk: Global env mutation is hard to trace. If another dependency also uses tar and expects Windows behavior, it will be silently broken.
  • Suggested fix: Consider scoping this to the Arborist child process only, or documenting it prominently as a known process-wide side effect.

[P1] packages/core/src/effect/observability.tsserviceName: Runtime.appName() but opencode.client attribute remains

The carve-out changes serviceName from hardcoded "opencode" to Runtime.appName() (which returns "pawwork"). However, the OTel resource attributes still include "opencode.client", "opencode.process_role", "opencode.run_id".

  • Boundary case: If PawWork ever sends telemetry to an upstream opencode collector, the serviceName will say "pawwork" but the attributes will still say "opencode". This is inconsistent branding.
  • Regression risk: Low for functionality, but medium for telemetry attribution confusion.
  • Suggested fix: Either rename the attribute keys to pawwork.* or add a comment explaining why they remain opencode.* (e.g., upstream schema compatibility).

[P1] packages/core/src/npm.tsInstallFailedError schema change breaks external consumers

The error shape changed from { pkg: string } to { add: string[], dir: string, cause: Schema.Defect }. The PR updates the SDK types and OpenAPI snapshot, which is good.

  • Boundary case: Any external SDK consumer (third-party integrations, scripts) that parsed properties.pkg will break. The OpenAPI spec change is a breaking API change.
  • Regression risk: If there are downstream consumers outside this repo (e.g., cloud control plane, mobile app), they need coordinated updates.
  • Suggested fix: Add a note to the PR description or a migration doc about the breaking SDK change. Consider whether pkg should be kept as a deprecated alias for backward compatibility.

[P2] packages/core/src/global.tsxdg-basedir modules may return undefined

const data = path.join(xdgData!, app)

The code uses non-null assertion (!) on xdgData, xdgCache, etc. In exotic environments (Windows without WSL, containers without XDG vars, certain CI images), these can return undefined.

  • Boundary case: On Windows or minimal Docker containers, xdg-basedir may return undefined for some paths, causing path.join(undefined, "pawwork") to throw.
  • Regression risk: Crash on startup in unsupported environments. The old code had the same issue, so this is not a new regression, but the move to core makes it harder to patch locally.
  • Suggested fix: Add fallback values (e.g., path.join(os.homedir(), ".local", "share")) or at least a runtime check with a clear error message.

[P2] packages/core/src/npm.tsresolveEntryPoint uses import.meta.resolve with Bun-only guard

const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
  • Boundary case: In Node.js (non-Bun) environments, import.meta.resolve(dir) resolves the directory itself, not the package name. This is likely incorrect — it should resolve name with dir as the parent.
  • Regression risk: If PawWork ever runs on Node.js (e.g., for debugging, testing, or future portability), resolveEntryPoint will return wrong paths.
  • Suggested fix: Use require.resolve(name, { paths: [dir] }) for Node.js, or document that this module is Bun-only.

[P2] packages/core/src/npm-config.ts — imports from @npmcli/config/lib/definitions/index.js

import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"

This imports from a deep internal path that npm does not guarantee as stable API.

  • Boundary case: A future npm patch release could move or rename this file, breaking the build.
  • Regression risk: Build breakage on bun install when npm updates. The // @ts-expect-error comment acknowledges this, but it doesn't prevent runtime breakage.
  • Suggested fix: Pin @npmcli/config to an exact version in package.json (already done: 10.8.1), and add a CI check or dependabot rule to block automatic minor/patch bumps for this package.

[P2] packages/core/src/util/opencode-process.ts — env var names still say OPENCODE

export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID"
export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE"

These are now in @opencode-ai/core but the env var names are still OPENCODE_*. This is inconsistent with the PawWork carve-out philosophy.

  • Boundary case: If a user runs both opencode and pawwork on the same machine, these env vars will collide.
  • Regression risk: Very low, but it’s a branding inconsistency that will need to be carved out in every future sync.
  • Suggested fix: Add these to the permanent carve-out list and rename to PAWWORK_RUN_ID / PAWWORK_PROCESS_ROLE, or document why they must stay OPENCODE_*.

[P2] packages/core/test/npm.test.ts — test uses Bun.write and fs.stat but no cleanup on failure

await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")

The tmpdir helper uses Symbol.asyncDispose, which is good. But if Bun.write or Npm.install throws, the using block should still dispose. However, Bun.write is not wrapped in try/finally.

  • Boundary case: Not a real issue because using handles disposal, but Bun.write may leave partial files if interrupted.
  • Regression risk: Minimal — test-only.
  • Suggested fix: None critical, but consider using fs.writeFile for consistency with the rest of the test suite (the cross-spawn test already switched from Bun.write to fs.writeFile).

[P3] packages/core/src/npm.tsNpm.install early-return on !canWrite without logging

if (!canWrite) return

If the target directory is not writable, Npm.install silently returns without installing anything and without logging.

  • Boundary case: Read-only project directories (e.g., mounted volumes, protected workspaces).
  • Regression risk: Users may wonder why dependencies are missing with no error.
  • Suggested fix: Add a log.warn or similar before the early return.

[P3] packages/core/package.jsonoverrides: { "drizzle-orm": "catalog:" } unexplained

The new overrides field in packages/core/package.json pins drizzle-orm to the catalog version. core does not appear to depend on drizzle-orm directly.

  • Boundary case: This may be a transitive override to resolve a version conflict elsewhere in the workspace.
  • Regression risk: If the override is unnecessary, it adds maintenance burden. If it is necessary, it should be documented.
  • Suggested fix: Add a comment in the PR or a package.json comment explaining why this override exists.

[P3] packages/core/src/effect/observability.tsAsyncLocalStorageContextManager registered globally without cleanup

const mgr = new AsyncLocalStorageContextManager()
mgr.enable()
context.setGlobalContextManager(mgr)

This is called inside the traces() async function, which is invoked once when the observability layer is built. There is no mgr.disable() or cleanup on process exit.

  • Boundary case: In test suites that create and tear down multiple runtimes, the global context manager may accumulate state or interfere with other tests.
  • Regression risk: Test flakiness in observability-enabled test runs.
  • Suggested fix: Add a cleanup hook or document that observability layer should not be rebuilt in tests.

[P3] packages/app/src/context/global-sync.tsxadd?.[0] fallback may show empty string

pkg: properties?.add?.[0] ?? "",

If add is an empty array, add?.[0] is undefined, so the toast description will show an empty package name.

  • Boundary case: InstallFailedError constructed with add: [] (theoretically possible via the schema since add is optional).
  • Regression risk: User sees "Failed to install " with no package name.
  • Suggested fix: Use properties?.add?.[0] ?? properties?.dir ?? "unknown" for a better fallback.

[P3] packages/core/src/npm.tsreify uses Effect.withSpan but add is not sanitized in span attributes

Effect.withSpan("Npm.reify", { attributes: input })

If input.add contains a scoped package with a registry token (e.g., @acme/pkg@npm:registry.example.com/pkg?authToken=secret), the token will be emitted into the OTel span.

  • Boundary case: Private registry URLs with embedded credentials.
  • Regression risk: Credential leakage in telemetry.
  • Suggested fix: Sanitize or redact add entries before putting them in span attributes.

[P3] packages/opencode/src/lsp/server.tsnpmWhich catches Npm.InstallFailedError but re-throws other errors

if (err instanceof Npm.InstallFailedError) { ... }
throw err

If Npm.add throws a non-InstallFailedError (e.g., EffectFlock.LockError, unexpected runtime error), it bubbles up and may crash the LSP server initialization.

  • Boundary case: File system lock contention or disk-full errors.
  • Regression risk: LSP server crash instead of graceful degradation.
  • Suggested fix: Consider catching all errors in npmWhich, logging them, and returning undefined to match the "which returns undefined on failure" contract.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR moves Global, log, flag, cross-spawn-spawner, npm, and observability into @opencode-ai/core. The mechanical import rewrites look correct, and the carve-outs for PawWork branding are well-documented. Below are inline-style comments organized by severity.


[P0] packages/core/src/global.ts — top-level await in module scope

The new global.ts performs top-level await Promise.all([fs.mkdir(...)]) and calls Flock.setGlobal({ state }) at import time. This is a significant behavior change from the previous Layer.effect-based lazy initialization.

  • Boundary case: In test environments where OPENCODE_TEST_HOME is set after the module is first imported (e.g., via vitest/bun test preload patterns), the paths will already be computed against the real home directory. The previous Layer.effect deferred this until Effect.runSync or similar.
  • Regression risk: Any test that imports @opencode-ai/core/global transitively before setting OPENCODE_TEST_HOME will pollute the real ~/.config/pawwork/ or ~/.cache/pawwork/ directories.
  • Suggested fix: Move the eager mkdir and Flock.setGlobal into a Global.init() function that callers invoke explicitly, or keep them inside the Layer.effect so initialization is lazy and test-controllable.

[P0] packages/core/src/npm.tsNpm.which swallows all errors silently

export async function which(...args) {
  const resolved = await runPromise((svc) => svc.which(...args))
  return Option.getOrUndefined(resolved)
}

The upstream which() implementation returns Option.none() on any failure (including InstallFailedError, EffectFlock.LockError, file system errors, etc.). The PR description acknowledges this and adds npmWhich() in lsp/server.ts to re-emit InstallFailedError.

  • Boundary case: Non-LSP callers of Npm.which (e.g., Formatter in packages/opencode/src/format/formatter.ts, Provider in provider/provider.ts) now lose all failure signals. Previously, Npm.which in the old packages/opencode/src/npm/index.ts would throw on add() failure, letting callers handle it.
  • Regression risk: Formatter or provider resolution may silently fail without any log or user feedback. The npmWhich wrapper only covers LSP servers.
  • Suggested fix: Either restore error propagation in Npm.which, or audit all non-LSP callers to ensure silent failure is acceptable.

[P1] packages/core/src/npm.tsloadArborist mutates process.env globally

if (process.platform === "win32") {
  process.env.__FAKE_PLATFORM__ = "linux"
}

This is set at module load time inside loadArborist(), which is called lazily. However, once set, it persists for the entire process.

  • Boundary case: If PawWork later needs to run actual tar operations on Windows (e.g., packaging, extracting user archives), __FAKE_PLATFORM__=linux may cause incorrect behavior in those tar instances.
  • Regression risk: Global env mutation is hard to trace. If another dependency also uses tar and expects Windows behavior, it will be silently broken.
  • Suggested fix: Consider scoping this to the Arborist child process only, or documenting it prominently as a known process-wide side effect.

[P1] packages/core/src/effect/observability.tsserviceName: Runtime.appName() but opencode.client attribute remains

The carve-out changes serviceName from hardcoded "opencode" to Runtime.appName() (which returns "pawwork"). However, the OTel resource attributes still include "opencode.client", "opencode.process_role", "opencode.run_id".

  • Boundary case: If PawWork ever sends telemetry to an upstream opencode collector, the serviceName will say "pawwork" but the attributes will still say "opencode". This is inconsistent branding.
  • Regression risk: Low for functionality, but medium for telemetry attribution confusion.
  • Suggested fix: Either rename the attribute keys to pawwork.* or add a comment explaining why they remain opencode.* (e.g., upstream schema compatibility).

[P1] packages/core/src/npm.tsInstallFailedError schema change breaks external consumers

The error shape changed from { pkg: string } to { add: string[], dir: string, cause: Schema.Defect }. The PR updates the SDK types and OpenAPI snapshot, which is good.

  • Boundary case: Any external SDK consumer (third-party integrations, scripts) that parsed properties.pkg will break. The OpenAPI spec change is a breaking API change.
  • Regression risk: If there are downstream consumers outside this repo (e.g., cloud control plane, mobile app), they need coordinated updates.
  • Suggested fix: Add a note to the PR description or a migration doc about the breaking SDK change. Consider whether pkg should be kept as a deprecated alias for backward compatibility.

[P2] packages/core/src/global.tsxdg-basedir modules may return undefined

const data = path.join(xdgData!, app)

The code uses non-null assertion (!) on xdgData, xdgCache, etc. In exotic environments (Windows without WSL, containers without XDG vars, certain CI images), these can return undefined.

  • Boundary case: On Windows or minimal Docker containers, xdg-basedir may return undefined for some paths, causing path.join(undefined, "pawwork") to throw.
  • Regression risk: Crash on startup in unsupported environments. The old code had the same issue, so this is not a new regression, but the move to core makes it harder to patch locally.
  • Suggested fix: Add fallback values (e.g., path.join(os.homedir(), ".local", "share")) or at least a runtime check with a clear error message.

[P2] packages/core/src/npm.tsresolveEntryPoint uses import.meta.resolve with Bun-only guard

const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
  • Boundary case: In Node.js (non-Bun) environments, import.meta.resolve(dir) resolves the directory itself, not the package name. This is likely incorrect — it should resolve name with dir as the parent.
  • Regression risk: If PawWork ever runs on Node.js (e.g., for debugging, testing, or future portability), resolveEntryPoint will return wrong paths.
  • Suggested fix: Use require.resolve(name, { paths: [dir] }) for Node.js, or document that this module is Bun-only.

[P2] packages/core/src/npm-config.ts — imports from @npmcli/config/lib/definitions/index.js

import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"

This imports from a deep internal path that npm does not guarantee as stable API.

  • Boundary case: A future npm patch release could move or rename this file, breaking the build.
  • Regression risk: Build breakage on bun install when npm updates. The // @ts-expect-error comment acknowledges this, but it doesn't prevent runtime breakage.
  • Suggested fix: Pin @npmcli/config to an exact version in package.json (already done: 10.8.1), and add a CI check or dependabot rule to block automatic minor/patch bumps for this package.

[P2] packages/core/src/util/opencode-process.ts — env var names still say OPENCODE

export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID"
export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE"

These are now in @opencode-ai/core but the env var names are still OPENCODE_*. This is inconsistent with the PawWork carve-out philosophy.

  • Boundary case: If a user runs both opencode and pawwork on the same machine, these env vars will collide.
  • Regression risk: Very low, but it's a branding inconsistency that will need to be carved out in every future sync.
  • Suggested fix: Add these to the permanent carve-out list and rename to PAWWORK_RUN_ID / PAWWORK_PROCESS_ROLE, or document why they must stay OPENCODE_*.

[P2] packages/core/test/npm.test.ts — test uses Bun.write and fs.stat but no cleanup on failure

await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")

The tmpdir helper uses Symbol.asyncDispose, which is good. But if Bun.write or Npm.install throws, the using block should still dispose. However, Bun.write is not wrapped in try/finally.

  • Boundary case: Not a real issue because using handles disposal, but Bun.write may leave partial files if interrupted.
  • Regression risk: Minimal — test-only.
  • Suggested fix: None critical, but consider using fs.writeFile for consistency with the rest of the test suite (the cross-spawn test already switched from Bun.write to fs.writeFile).

[P3] packages/core/src/npm.tsNpm.install early-return on !canWrite without logging

if (!canWrite) return

If the target directory is not writable, Npm.install silently returns without installing anything and without logging.

  • Boundary case: Read-only project directories (e.g., mounted volumes, protected workspaces).
  • Regression risk: Users may wonder why dependencies are missing with no error.
  • Suggested fix: Add a log.warn or similar before the early return.

[P3] packages/core/package.jsonoverrides: { "drizzle-orm": "catalog:" } unexplained

The new overrides field in packages/core/package.json pins drizzle-orm to the catalog version. core does not appear to depend on drizzle-orm directly.

  • Boundary case: This may be a transitive override to resolve a version conflict elsewhere in the workspace.
  • Regression risk: If the override is unnecessary, it adds maintenance burden. If it is necessary, it should be documented.
  • Suggested fix: Add a comment in the PR or a package.json comment explaining why this override exists.

[P3] packages/core/src/effect/observability.tsAsyncLocalStorageContextManager registered globally without cleanup

const mgr = new AsyncLocalStorageContextManager()
mgr.enable()
context.setGlobalContextManager(mgr)

This is called inside the traces() async function, which is invoked once when the observability layer is built. There is no mgr.disable() or cleanup on process exit.

  • Boundary case: In test suites that create and tear down multiple runtimes, the global context manager may accumulate state or interfere with other tests.
  • Regression risk: Test flakiness in observability-enabled test runs.
  • Suggested fix: Add a cleanup hook or document that observability layer should not be rebuilt in tests.

[P3] packages/app/src/context/global-sync.tsxadd?.[0] fallback may show empty string

pkg: properties?.add?.[0] ?? "",

If add is an empty array, add?.[0] is undefined, so the toast description will show an empty package name.

  • Boundary case: InstallFailedError constructed with add: [] (theoretically possible via the schema since add is optional).
  • Regression risk: User sees "Failed to install " with no package name.
  • Suggested fix: Use properties?.add?.[0] ?? properties?.dir ?? "unknown" for a better fallback.

[P3] packages/core/src/npm.tsreify uses Effect.withSpan but add is not sanitized in span attributes

Effect.withSpan("Npm.reify", { attributes: input })

If input.add contains a scoped package with a registry token (e.g., @acme/pkg@npm:registry.example.com/pkg?authToken=secret), the token will be emitted into the OTel span.

  • Boundary case: Private registry URLs with embedded credentials.
  • Regression risk: Credential leakage in telemetry.
  • Suggested fix: Sanitize or redact add entries before putting them in span attributes.

[P3] packages/opencode/src/lsp/server.tsnpmWhich catches Npm.InstallFailedError but re-throws other errors

if (err instanceof Npm.InstallFailedError) { ... }
throw err

If Npm.add throws a non-InstallFailedError (e.g., EffectFlock.LockError, unexpected runtime error), it bubbles up and may crash the LSP server initialization.

  • Boundary case: File system lock contention or disk-full errors.
  • Regression risk: LSP server crash instead of graceful degradation.
  • Suggested fix: Consider catching all errors in npmWhich, logging them, and returning undefined to match the "which returns undefined on failure" contract.

Pulls upstream PR follow-ups to the shared→core rename:
- 1a734adb4d core: consolidate shared infrastructure into core package
- 705f792e87 core: move Global module to @opencode-ai/core
- 3eee2f6afa core: move cross-spawn-spawner from opencode to core
- 1e98167b0e core: move cross-spawn-spawner to root and remove unused types
- f5dce6d960 core: move npm service to core package

Also pulls the npm-config snapshot from upstream HEAD (Stage C of #209
intake plan): packages/core/src/npm.ts, npm-config.ts, npm-config.test.ts.
The pre-existing packages/opencode/test/npm.test.ts is removed.

PawWork carve-outs:
- packages/core/src/global.ts uses Runtime.appName() instead of
  hardcoded "opencode" so the pawwork-namespace data dir is preserved.
- packages/opencode/src/global/index.ts is kept as PawWork's local
  Runtime-aware Global module; upstream's auto-rewritten
  @opencode-ai/core/global imports flow through to the carve-out.
- packages/core/src/effect/logger.ts uses PawWork's namespace-style Log
  API (Log.create / Log.Default / Log.init) rather than upstream HEAD's
  flat-export style. PawWork callers expect the namespace shape.

Mechanical cleanup:
- packages/opencode/src/installation/meta.ts is removed; callers use
  @opencode-ai/core/installation/version (InstallationVersion /
  InstallationChannel) directly.
- All ../util/log, ../flag/flag, @/effect/* and similar local imports
  rewritten to @opencode-ai/core/* equivalents (~120 files).
- @effect/opentelemetry catalog entry added to root package.json
  (4.0.0-beta.46, matching the rest of the effect ecosystem).

Verified: typecheck clean across packages/{core, opencode, util, ui,
desktop-electron}.

Refs: #209
Two behaviors silently dropped when packages/opencode/src/npm/index.ts
was replaced by upstream HEAD's packages/core/src/npm.ts in the prior
move commit:

1. Windows __FAKE_PLATFORM__ workaround. Bun on Windows lacks
   UV_FS_O_FILEMAP that tar uses for small-file IO; tar reads
   __FAKE_PLATFORM__ once on its module init, so we set it before
   the first @npmcli/arborist import (which transitively loads tar).
   Without this, LSP package installs fail on Windows.

2. arborist.loadVirtual() integrity check before trusting the cache
   directory. existsSafe(dir) alone treats interrupted reifies and
   missing node_modules inner files as valid, returning a stale or
   broken entry point. loadVirtual reads and validates the package
   tree, so corrupted caches fall through to a fresh reify.

Refs: #209
@Astro-Han Astro-Han force-pushed the claude/upstream-sync-209-pr2-moves branch from 9386bd8 to 6f1633f Compare April 27, 2026 17:52
@Astro-Han

Copy link
Copy Markdown
Owner Author

Cross-checked all 17 findings against pre-PR2 baseline (aed455918:packages/opencode/src/global/index.ts etc.) and current PR state. Pushing back on all of them — most rest on a wrong premise about pre-move behavior, a few are upstream patterns, and the rest are YAGNI for this mechanical-move PR.

# Verdict Reason
P0-1 top-level await in global.ts Push back Pre-existing upstream pattern. git show aed455918:packages/opencode/src/global/index.ts already has await Promise.all([fs.mkdir(...)]) at module scope. PR2 moved the file unchanged. The "previous Layer.effect-based lazy initialization" premise is wrong. PawWork's test setup uses OPENCODE_TEST_HOME which is set before process start (memory: reference_bun_homedir_lock.md).
P0-2 Npm.which swallows errors Push back Verified non-LSP callers: only packages/opencode/src/format/formatter.ts (3 callsites: prettier/oxfmt/@biomejs/biome). All use const bin = await Npm.which(...); if (bin) return [bin, ...] — formatter quietly disabled when bin missing, which is correct UX (best-effort feature). No Npm.which in provider/provider.ts. LSP keeps its wrapper. No regression.
P1-1 __FAKE_PLATFORM__ global env mutation Push back Documented carve-out for tar's UV_FS_O_FILEMAP issue on Bun+Windows. tar reads __FAKE_PLATFORM__ once at module init, so persistence is required, not accidental. The comment in loadArborist() explains exactly this.
P1-2 serviceName vs opencode.client attribute inconsistency Push back Attribute keys are upstream's OTel schema, not introduced by this PR. The serviceName carve-out is targeted; broader attribute-key rename is a separate cleanup that needs its own decision (telemetry collector compatibility, dashboard queries). Not part of mechanical-move scope.
P1-3 InstallFailedError SDK breaking change Already done SDK is updated in this PR — packages/sdk/openapi.json and packages/sdk/js/src/v2/gen/types.gen.ts both modified. No external SDK consumers outside this repo. No additional code change needed.
P2-1 xdg-basedir ! non-null assertion Push back Pre-existing in aed455918:packages/opencode/src/global/index.ts:9-12. Mechanical move preserved upstream pattern. Out of scope for PR2.
P2-2 import.meta.resolve Bun-only Push back Upstream pattern: PawWork ships as a Bun-compiled binary (packages/opencode/script/build.ts), so the Bun-only assumption is intentional. No Node.js runtime target.
P2-3 deep import from @npmcli/config/lib/definitions Push back Upstream pattern. Pin already in place (10.8.1 exact). // @ts-expect-error comment documents the awareness. Adding a CI check for transitive npm bumps is out-of-scope tooling work.
P2-4 OPENCODE_RUN_ID env var names Push back Internal runtime metadata for OTel process correlation, not user-facing CLI env. No collision risk with the opencode binary because each process owns its own env. Renaming would affect telemetry attribution continuity across upstream syncs.
P2-5 test cleanup with Bun.write Push back Test-only. using handles disposal. Switching to fs.writeFile is a stylistic preference, not a fix.
P3-1 !canWrite silent return Push back Upstream pattern. Read-only project dirs is an intentional opt-out for Npm.install (cache reads still work). Adding warn log is upstream territory.
P3-2 drizzle-orm override Push back Upstream's, not ours. git show 826a5f74b:packages/core/package.json shows the override predates this PR. Not introduced here.
P3-3 AsyncLocalStorageContextManager no cleanup Push back PawWork carve-out (packages/core/src/effect/observability.ts) — global registration is intentional because the AI SDK's tracer.startActiveSpan() uses context.active() from the global API. The comment block above the registration explains this. Not test-relevant since observability is gated by OTEL_EXPORTER_OTLP_ENDPOINT.
P3-4 add?.[0] ?? "" fallback Push back YAGNI. npmWhich always passes pkg to Array.from(err.add ?? [pkg]), so add[0] is guaranteed non-empty. The ?? "" is already defense-in-depth for an impossible path.
P3-5 Npm.reify span attributes leak secrets Push back Theoretical. add entries here are package names from package.json deps + LSP server bundles, not user-provided registry URLs with auth tokens. Authentication tokens live in .npmrc (handled by NpmConfig.load), not in package specifiers.
P3-6 npmWhich rethrows non-InstallFailedError Push back Intentional. Catching everything would swallow programmer errors and OOM/disk-full as "feature unavailable". EffectFlock.LockError is a recoverable known type — if it becomes a real LSP failure pattern, we'd add it explicitly to the catch, not blanket-catch.

If any of these reads as defensiveness rather than reasoning, name the specific finding and I'll re-verify.

After the Npm move to @opencode-ai/core, core/npm.ts has no Bus access
and core/npm.ts which() swallows InstallFailedError, leaving the
existing app/src/context/global-sync.tsx toast listener as dead code.

Restores the user-visible install-failure feedback by adding an
opencode-side npmWhich helper that drives Npm.add explicitly, catches
InstallFailedError, and publishes LSP.Event.InstallFailed before
falling back to undefined (matching upstream which()'s contract).

The new InstallFailedError schema is {add, dir, cause} (vs old {pkg}),
so the Bus event payload, openapi snapshot, and SDK type generation
update to match: {add: string[], dir: string, error: string}. The UI
listener now reads add[0] for the toast description.

Refs: #209
Three unused artifacts left behind by the upstream HEAD npm.ts /
observability.ts pull:

- packages/opencode/src/effect/oltp.ts — pre-move Observability
  namespace; nothing imports it after the barrel and run-service.ts
  switched to @opencode-ai/core/effect/observability. Deleting
  prevents an accidental IDE auto-import from wiring up the stale
  loggerMergeWithExisting/loggerExportInterval implementation.

- packages/core/package.json bin entry — points to ./bin/opencode
  which doesn't exist in this package; old packages/shared/package.json
  also had no bin entry. Belongs in packages/opencode/package.json.

- npm-package-arg dependency — the carve-out commit removed the only
  npa() call site from core/npm.ts; opencode keeps its own dep entry
  for plugin/shared.ts.

Refs: #209
Two gaps in the upstream HEAD observability.ts pull:

1. Missing @opentelemetry/sdk-trace-node runtime peer. The
   @effect/opentelemetry/NodeSdk import that observability.ts uses
   transitively requires @opentelemetry/sdk-trace-node, which the
   desktop-electron build resolves at build time. Without it, the
   build fails with "Could not resolve: @opentelemetry/sdk-trace-node"
   on every smoke run.

2. serviceName: "opencode" hardcoded. The PR already carves out
   global.ts to use Runtime.appName() so PawWork keeps its own
   namespace; the OTel resource attribution should follow the same
   boundary so PawWork telemetry doesn't get attributed as opencode.

Refs: #209
After packages/opencode/src/util/log.ts moved into
@opencode-ai/core/util/log, the lazy import in
packages/app/script/e2e-local.ts kept pointing at the old path and
the e2e smoke job fails with "Cannot find module
'../../opencode/src/util/log'". Switch to the package import like
the rest of the repo.

Refs: #209
The local opencode/src/global/index.ts and the new
@opencode-ai/core/global were two independent module instances with
their own Path objects. Tests that mutated Global.Path.config via the
local module had no effect on config.ts, which had been rewritten by
the move to read Global.Path.config from core. The PR description's
claim that core imports "flow through to the carve-out" was not
actually wired up.

Slim opencode/src/global/index.ts down to a re-export of core's Global
namespace, keeping only the cache-version-check side effect that
core/global doesn't have. Mutations and singleton state now propagate.

Refs: #209
@Astro-Han Astro-Han force-pushed the claude/upstream-sync-209-pr2-moves branch from 6f1633f to 5e335cb Compare April 27, 2026 18:02
@Astro-Han

Copy link
Copy Markdown
Owner Author

Re-审一遍后调整:P3-4 和 P3-6 纳入修复,已 squash 进 53e5bb61d(LSP wrapper commit)。其余 15 条仍 push back。

已修:

  • P3-6 npmWhich 兜底 LockErrorNpm.add 的错误通道含 EffectFlock.LockErrorLockTimeoutError | LockCompromisedError),都是已知可恢复型。原来 blanket throw 会让 LSP bootstrap 因为 lock 竞争直接崩溃。改成显式 instanceof EffectFlock.LockTimeoutError | LockCompromisedError → log.warn + return undefined,与 which() "feature unavailable" 契约一致;其他未知错误仍然抛上去保留 debugging 能力。
  • P3-4 toast pkg fallback:改成 ?? properties?.dir ?? "unknown",避免极端情况下 toast 显示残缺串。是 cosmetic 但 1 行成本。

重新评估后仍 push back 的:

  • P1-2 attribute key 一致性:本来想顺手改,verify 后发现:opencode.client attribute 读的是 Flag.OPENCODE_CLIENT env var。改 attribute 不改 env 是误导,改 env 又会牵动 CLI flag、用户文档、packages/core/test/effect/observability.test.ts 等。完整 carve-out 范围远大于 attribute key 本身,需要独立 issue 决策(telemetry 收集器兼容、dashboard 查询语句迁移等)。serviceName 这一层先单独立住。

其余 14 条理由同上次评论。

@Astro-Han Astro-Han merged commit ae1e1d7 into dev Apr 27, 2026
32 of 34 checks passed
@Astro-Han Astro-Han deleted the claude/upstream-sync-209-pr2-moves branch April 27, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

harness Model harness, prompts, tool descriptions, and session mechanics P3 Low priority upstream Tracked upstream or vendor behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant