Redrob Code: the engine behind Redrob Cowork, a fork of OpenCode. It is consumed as a pinned
version rather than a branch — constants.json in redrob-cowork names the release the desktop app
ships, so a change here reaches users only through a release and a pin bump, never by merging.
Per-package instructions live beside the code they describe and are more specific than this file.
There are ten of them; list them with git ls-files | grep 'AGENTS.md$'. Read the nearest one
before editing a package — packages/redrob/, packages/redrob/test/, packages/schema/,
packages/llm/ and packages/codemode/ each have their own, and three sit deeper under
packages/redrob/src/**.
Gitflow. develop is the default branch and the base of every pull request.
Branch names are <type>/<slug> with the type from the redrob-labs organization list:
feat fix chore docs test refactor perf ci build sync hotfix release (docs/BRANCHING.md has the
ones this repository actually uses: feat/<slug>, sync/upstream-<tag>, hotfix/<slug>,
release/<version>). Do not invent a prefix, and in particular do not name a branch after the tool
or agent that made it: a branch name says what the change is, not who typed it.
developintegrates. Release from it: thereleaseworkflow builds, signs, tags, publishes the release, and prints a link to open the promotion pull request fromdevelopintomain.mainis released state. It moves through a reviewed pull request, not an automated push.- Hotfix: branch from
main, merge intomain, release, then mergemainback intodevelop. This is the step that gets skipped. #13 fixed the lockfile onmain, the back-merge never happened, and for a monthbun install --frozen-lockfilefailed on the default branch. The shared gitflow workflow now opens the back-merge pull request (sync/back-merge-<sha>) on every push tomainthat leavesmainahead ofdevelop, so this is checked rather than remembered. - Both branches are protected: pull requests only, force pushes and deletions blocked, zero required
reviews, admin enforcement off. Required checks:
typecheck,unit (linux),core (linux),httpapi (linux).
Both rules above are enforced by CI, not by memory. gitflow.yml is a verbatim copy of the
organization's caller template and only calls redrob-labs/.github/.github/workflows/gitflow.yml,
which owns both checks: gitflow / branch name follows the convention on every pull request
(because a prefix nobody defined, kiro/, reached this repository while the rule sat in prose) and
gitflow / main is contained in develop on every push to main. Do not add a type list to the
caller; the one list lives in the organization repository. This repository's own tag rule (no
workflow pushes tags in bulk) is tag-hygiene.yml.
The invariant, readable by hand:
git rev-list --count origin/develop..origin/main # 0, outside a release windowFull detail in docs/BRANCHING.md.
- Use the pinned bun.
packageManagernames it, and the version matters more than it looks: bun 1.3.14 fails a stale lockfile withlockfile had changes, but lockfile is frozen, while 1.4.2 printsnote: skipped 1 workspace listed in bun.lock but not on diskand continues. A defect that only the pinned version reports is invisible to anyone running a newer one. bun install --frozen-lockfileis a real gate, not a formality. Delete a workspace and the lockfile must be regenerated in the same change.- The
pre-pushhook runsbun typecheckand asserts the bun version satisfies^packageManager``. Do not bypass it with--no-verify; if it cannot run, put bun and `node_modules/.bin` on PATH instead. - Judge by the exit code, not by a pass count. A suite can print
0 failand still exit non-zero when a file fails to load: its tests are never counted and a(fail)grep finds nothing. - The version line is ours and starts at
0.1.0. It does not encode the upstream version, which lives inUPSTREAM_VERSIONat the repository root and is what the release notes read. Do not reintroduce a-redrob.Nsuffix: it made every release a semver prerelease, which sorts below the release it precedes and madegetReleaseType()call everything a patch. Seedocs/VERSIONING.md.
The engine stores what the console reports, and the app reads it back. When a field goes missing at one hop the symptom appears at another, so trace the whole chain rather than the endpoint:
console redrobBlock() -> protocol RedrobGatewayBlock -> getUsage -> processor step-finish
-> v1/v2 Assistant -> app readMessageUsage
A cast that narrows a payload must name every field it carries. One that did not is how streaming callers silently lost the billed cost while non-streaming callers kept it.