Before creating any git commit, you MUST run just pre-commit.
Only proceed with the commit if it passes. If it fails, fix the issues first.
Exception: If the commit contains only documentation and markdown changes (.md files), you may skip just pre-commit.
just pre-commit intentionally runs generation and formatting steps that may modify files you did not edit directly (including markdown). Treat these diffs as expected outputs of the check pipeline.
just pre-commit also runs just check-opencode-guardrails-autofix, which may auto-sync fork-boundary manifest drift in packages/opencode/docs/upstream-sync/fork-boundary-manifest.json when drift is the only issue.
If those diffs are mechanical (generation/format/lint output only), they should be committed with the related change. Do not treat them as noise and do not leave them uncommitted.
Common expected examples (non-exhaustive):
packages/opencode/packages/sdk/openapi.jsonpackages/opencode/README.md(including normalized markdown formatting / generated sections)packages/core/README.md(synced by hook)bun.lockfiles
Required post-just pre-commit checklist:
- Check
git status(superproject) andgit -C packages/opencode status(submodule). - Review all newly changed files and confirm they are mechanical (no unintended behavior changes).
- Commit/push submodule generated changes first when present.
- Run
just update-opencode-commit. - Commit/push the superproject pointer + Dockerfile pin update when changed.
bun.lock file updates are expected in this repository across all bun.lock files when changing versions of our own packages (for example packages/cli-node, packages/core, and related workspace packages). They can also be produced by normal just pre-commit/build flows.
When these bun.lock changes are tied to intended package/version updates, they are valid and should be committed with the related change. Do not treat them as unexpected noise.
This same expected-and-commit rule applies to other mechanical formatter/generator outputs from just pre-commit, including markdown rewrites.
This is a polyglot monorepo with Rust and TypeScript:
packages/core/- Rust core library with NAPI-RS bindingspackages/cli-rust/- Rust CLI binarypackages/cli-node/- Node.js CLI wrapperpackages/opencode/- Git submodule pointing to our fork of the opencode repository. We own this fork and can freely modify, commit, and push changes to it.
The packages/opencode/ submodule is a fork that syncs from upstream every 2 hours. To minimize merge conflicts:
- Prefer putting new code in
fork-*packages (fork-auth,fork-config,fork-ui,fork-security,fork-terminal,fork-cli,fork-provider,fork-tests). These are ours and never conflict with upstream. - Minimize modifications to non-fork packages (
app,opencode,sdk,ui,util). When changes to upstream code are needed, keep the diff as small as possible — typically just an import and a single function call that delegates to afork-*package. - Do not refactor upstream code. Even if upstream code has bugs or poor patterns, fix only what directly blocks our users. Leave broader cleanups to upstream — they will fix their own bugs over time. Every line we change in upstream files is a potential merge conflict.
- Tolerate imperfect upstream code. If an upstream bug doesn't affect our users, leave it alone. If it does affect our users, fix it with the smallest possible diff (inline changes > new imports > new files).
- Use thin re-export shims in upstream packages that delegate to
fork-*implementations when hooks are needed.
just setup # First command after clone/worktree init (hooks + deps + submodule bootstrap)
just build # Build all packages
just test # Run all tests
just e2e # Run e2e tests (Playwright, boots server in-process)
just fmt # Format all code
just lint # Lint all code
just pre-commit # Format, lint, build, test, and e2e
just clean # Clean build artifacts
just run <args> # Run CLI with arguments (e.g., just run status)
just dev # Start local runtime (local submodule + cached sandbox rebuild)Run bash scripts/check-dev-prereqs.sh to verify all required and optional tools are installed. Then run just setup before any other project command after cloning this repo. In new worktrees, initialize the submodule first with git submodule update --init --recursive, then run just setup.
Setup reference:
- Rust toolchain:
1.89(fromrust-toolchain.toml) - Bun:
1.3.9+ - Optional tools (
docker,jq,shellcheck,actionlint,cfn-lint) are required only for specific flows (just dev,just lint, and CloudFormation hook checks). - Rerun
just setupfor new clones/worktrees, if hooks are reset, or if dependency bootstrap looks stale.
This repo uses git hooks (wired via git config core.hooksPath .githooks, set up by just setup):
- pre-commit — Syncs README to npm packages, guards against unpublished submodule pins, runs cfn-lint on CloudFormation changes.
- pre-push — Validates the opencode submodule commit is published. When outgoing commits update the
packages/opencodegitlink, it also runs strict opencode guardrails (just check-opencode-guardrails) before push. - post-merge — After every
git pull, automatically syncs the submodule and runsbun install. No manual action needed.
README.md (superproject) and packages/opencode/README.md (submodule) use distinct generated badge blocks for different audiences.
The source of truth is packages/opencode/packages/fork-ui/src/readme-badge-catalog.ts.
- Run
just sync-readme-badgesafter badge catalog changes. just lintandjust pre-commitrunjust check-readme-badgesand fail on drift.- Do not manually edit badge lines between generated marker comments.
When performing manual UAT tests with the user, use justfile commands instead of the installed occ binary:
- Use
just run mount add /path:/containerinstead ofocc mount add /path:/container - Use
just run statusinstead ofocc status - Use
just devinstead ofocc start
This ensures tests run against the locally-built development version.
- npm package uses compile-on-install (no prebuilt binaries)
- Users need Rust 1.89+ installed for npm install
- Config stored at
~/.config/opencode-cloud/config.json - Data stored at
~/.local/share/opencode-cloud/
Important: packages/core/Cargo.toml must use explicit values (not workspace = true) because it's published to npm where users install it standalone without the workspace root.
When updating versions or metadata, keep these files in sync:
Cargo.toml(workspace root) -[workspace.package]sectionpackages/core/Cargo.toml- explicit values for version, edition, rust-version, license, repository, homepage, documentation, keywords, categories
The scripts/set-all-versions.sh script handles version updates automatically. For other metadata changes, update both files manually.
- Always use rebase pulls (e.g.,
git pull --rebase). - For pushes in the
packages/opencode/submodule (our own fork), default to thedevflow unless explicitly requested otherwise:- Rebase pull from
devfirst (for example:git pull --rebase origin dev). - Then push to the effective
devbranch/tip for that repository (for example:git push origin HEAD:dev).
- Rebase pull from
- For pushes in the
opencode-cloudsuperproject, default tomainunless explicitly requested otherwise:- Rebase pull from
mainfirst (for example:git pull --rebase origin main). - Then push to
main.
- Rebase pull from
- After committing and pushing changes in the
packages/opencode/submodule, runjust update-opencode-commitin the superproject to update the Dockerfile'sOPENCODE_COMMITpin. Commit and push the resulting Dockerfile change tomain. - After pulling the superproject, the
post-mergehook automatically syncs the submodule and runsbun install. Then runjust update-opencode-commit— if the Dockerfile changed, commit and push the update. - Resolving
OPENCODE_COMMITmerge conflicts: If a rebase or merge produces a conflict in the Dockerfile'sOPENCODE_COMMITline, do not manually pick a SHA. Instead, accept either side to clear the conflict markers, then runjust update-opencode-committo set the correct value from the current submodule state.
Git worktrees and submodules interact poorly. Submodule metadata is partially shared across worktrees, so careless commands in one worktree can break others.
Submodules are not automatically initialized per worktree. After every git worktree add, run this in the new worktree:
git submodule update --init --recursivegit submodule sync rewrites submodule.<name>.url in the shared .git/config, affecting all worktrees. It also mutates core.worktree paths in shared submodule metadata. Only run sync if the submodule remote URL has actually changed, and only from the main worktree with no other worktrees active.
If two worktree sessions run git submodule update at the same time, they race on shared metadata under .git/modules/ and .git/config. This causes intermittent "cannot be used without a working tree" errors and stale core.worktree paths. Serialize submodule operations across worktrees.
The submodule should always be detached at the superproject-pinned commit. If git submodule status shows a branch name instead of a detached SHA, something has gone wrong (likely a sync or manual checkout inside the submodule). Fix with:
git submodule update --recursivepackages/opencode/script/check-fork-boundary.ts and packages/opencode/script/sync-fork-boundary-manifest.ts resolve target refs as:
FORK_BOUNDARY_TARGET_REF(when explicitly set)- Current symbolic branch name
HEADfallback when detached
This branch-or-HEAD behavior prevents false passes from stale local dev refs in detached submodule states.
If you see errors like "cannot be used without a working tree" or submodule commands fail mysteriously, the submodule's core.worktree config is pointing to a wrong or deleted worktree path. Fix from the affected worktree:
git submodule deinit -f packages/opencode
git submodule update --init --recursivegit submodule status --recursive
git diff --submodule=logWorktrees with initialized submodules may resist normal removal. Use git worktree remove --force <path> followed by git worktree prune.
- Do run
git submodule update --init --recursivein every new worktree. - Do keep submodules detached and pinned unless intentionally updating the pointer.
- Do check
git submodule statusbefore committing. - Do serialize submodule operations — never run them concurrently across worktrees.
- Don't run
git submodule syncunless the remote URL has changed. - Don't run
git checkout <branch>inside the submodule working tree. - Don't assume submodules are ready in a fresh worktree.
- Don't ignore failed worktree removal; clean up stale metadata promptly.