This is the single procedure for importing
LodyAI/Lody into vendor/lody, proving the
renderer and daemon from the same tree, and opening the result for review.
vendor/lody/BLITZ-PATCHES.md is the seam and conflict manual;
vendor/lody/UPSTREAM.md records pins.
Open a pull request. Never merge to main unattended. Automation may
prepare and push the branch, but it must never enable auto-merge or call
gh pr merge. A green gate proves mechanics, not that BlitzOS wants every new
upstream behavior.
The target command is:
npm run lody:merge -- --ref <release-tag-or-main>It is not present in the root package.json yet. Use the manual equivalent in
this runbook for every row marked manual.
| Step | Status at HEAD | Target owner |
|---|---|---|
| Resolve the ref, pull the subtree, update pins and baselines, run gates, push, and open the PR | Manual until plan PR E | scripts/lody-merge.mjs through npm run lody:merge |
| Materialize the five CLI adapters at their gitlink SHAs | Automated by npm run lody:adapters:sync |
scripts/lody-sync-adapters.mjs and reviewed vendor/lody-adapters/ trees |
| Build, check, pack, and stamp the daemon from the merged tree | Automated by npm run lody:build and the image's lody-build stage |
shared scripts/lody-build-package.mjs |
| Run the real-daemon pair matrix against the PR artifact | Required in the lody-daemon CI job |
LODY_BUNDLE and LODY_CLAUDE_BINARY select the job's installed artifact |
| Serve and compare daemon provenance | Inspect BUILD.json locally until plan PR D |
/lody/build plus browser comparison; legacy boxes have no route |
| Publish and deploy after merge | Automated now by .github/workflows/canary.yml |
A vendor-only merge publishes a new daemon-inclusive payload and reuses the base image; the daemon archive digest is part of the payload version (docs/BOX-IMAGE.md) |
The shipping image builds and installs the daemon package from this same
vendored tree. There is no npm daemon release to select or bump during an
upstream merge. The renderer/daemon identity is the upstream commit and build
stamp, never the stale apps/cli/package.json version.
Start from current main in a clean worktree. Record the branch point and old
pin before changing anything.
git status --porcelain
npm ci
BRANCH_POINT=$(git rev-parse HEAD)
OLD_SHA=$(sed -n 's/| Pinned commit | `\([0-9a-f]\{40\}\)` |/\1/p' vendor/lody/UPSTREAM.md)
test -n "$OLD_SHA"
printf 'branch=%s\nold Lody=%s\n' "$BRANCH_POINT" "$OLD_SHA"Prefer a public release tag. main is allowed when the needed change has no
release, but label the PR UNRELEASED UPSTREAM and run the entire pair matrix.
LODY_REF=<release-tag-or-main>
LODY_URL=https://github.com/LodyAI/Lody
git fetch --no-tags "$LODY_URL" "$LODY_REF"
NEW_SHA=$(git rev-parse FETCH_HEAD^{commit})
git log --oneline "$OLD_SHA..$NEW_SHA"
git rev-list --count "$OLD_SHA..$NEW_SHA"Stop without opening a PR when the count is zero.
Derive the declared seam-file inventory from BLITZ-PATCHES.md, then prove it
covers every current upstream divergence. This avoids another hard-coded file
list drifting as seams are added or retired.
DECLARED_SEAM_FILES=$(mktemp)
ACTUAL_SEAM_FILES=$(mktemp)
UNDECLARED_SEAM_FILES=$(mktemp)
grep -oE '`(apps|packages)/[^`]+\.(ts|tsx|js|mjs)`' \
vendor/lody/BLITZ-PATCHES.md | tr -d '`' | sort -u | \
while IFS= read -r file; do
git cat-file -e "HEAD:vendor/lody/$file" 2>/dev/null && printf '%s\n' "$file"
done > "$DECLARED_SEAM_FILES"
git diff --name-only "$OLD_SHA" "$(git rev-parse HEAD:vendor/lody)" -- . \
':!UPSTREAM.md' ':!BLITZ-PATCHES.md' | sort -u > "$ACTUAL_SEAM_FILES"
comm -23 "$ACTUAL_SEAM_FILES" "$DECLARED_SEAM_FILES" \
> "$UNDECLARED_SEAM_FILES"
if [ -s "$UNDECLARED_SEAM_FILES" ]; then
sed 's/^/undeclared vendor divergence: /' "$UNDECLARED_SEAM_FILES" >&2
exit 1
fi
git diff --name-only "$OLD_SHA" "$NEW_SHA" -- $(cat "$DECLARED_SEAM_FILES")The last command is the review forecast: it reports declared seam files that upstream touched. Numbers from earlier merges are examples, not contracts. One large merge covered 152 upstream commits and required review of six seam files and 16 local hunks; a later merge may have none or many more.
Also inspect upstream changes around ambient IPC. The source audit in the gates is authoritative; this early search identifies likely human-review sites.
git diff "$OLD_SHA" "$NEW_SHA" -- packages/components/src | \
grep -E 'getIpcServices|onIpcEvent|sendIpc|sendLocalSessionControl|getPublicBrowserBridge|window\.ipc' || true--squash reconstructs history from the squash commit message's
git-subtree-split: <sha> trailer. Confirm the old marker is reachable before
asking Git to pull new content:
OLD_SUBTREE_COMMIT=$(git log HEAD --fixed-strings \
--grep="git-subtree-split: $OLD_SHA" --format='%H' -1)
if [ -z "$OLD_SUBTREE_COMMIT" ]; then
git subtree pull --prefix vendor/lody "$LODY_URL" "$OLD_SHA" --squash
OLD_SUBTREE_COMMIT=$(git log HEAD --fixed-strings \
--grep="git-subtree-split: $OLD_SHA" --format='%H' -1)
fi
test -n "$OLD_SUBTREE_COMMIT"That old-pin pull is the recovery when an earlier rewrite lost the split trailer. If it cannot establish the marker, re-add the old subtree in a scratch worktree and compare it before proceeding. Never accept a pull that appears to re-import the whole tree.
Now pull the resolved new commit:
git subtree pull --prefix vendor/lody "$LODY_URL" "$NEW_SHA" --squashIf the new pull reports conflicts, skip the new marker check for the moment, resolve every conflict by the next section, then finish the prepared subtree commit without rewriting its message:
git add vendor/lody
git commit --no-editReturn here and verify the marker before doing any other merge work.
Preserve the new marker:
- Never amend, reword, rebase, or squash the subtree squash commit.
- Land the PR with a merge commit. Do not use GitHub's squash or rebase merge.
- Verify the marker before continuing.
SUBTREE_COMMIT=$(git log HEAD --fixed-strings \
--grep="git-subtree-split: $NEW_SHA" --format='%H' -1)
test -n "$SUBTREE_COMMIT"
git show -s --format='%B' "$SUBTREE_COMMIT" | \
grep -F "git-subtree-dir: vendor/lody"
git show -s --format='%B' "$SUBTREE_COMMIT" | \
grep -F "git-subtree-split: $NEW_SHA"Open vendor/lody/BLITZ-PATCHES.md for every conflict. Conflicts outside a
declared seam are evidence of an undeclared vendor edit; stop and report them.
| Class | Meaning | Resolution |
|---|---|---|
| A | Upstream now supplies the behavior | Delete the Blitz seam and its declaration; use upstream's API |
| B | The behavior is still needed and only its source anchor moved | Reapply the same narrow behavior using that seam's conflict drill; update anchors and tests |
| C | Upstream changed the mechanism or product meaning | Stop for a human decision; do not invent a reconciliation |
After the tree is resolved, update vendor/lody/UPSTREAM.md with NEW_SHA,
the upstream commit date/title, the current date, and SUBTREE_COMMIT. Do not
add an npm row.
Diff the upstream commit against the vendored tree, not against the Blitz
squash commit. The upstream commit and HEAD:vendor/lody both expose Lody paths
at the tree root; the squash commit does not.
git diff --stat "$NEW_SHA" "$(git rev-parse HEAD:vendor/lody)" -- . \
':!UPSTREAM.md' ':!BLITZ-PATCHES.md'
git diff --name-only "$NEW_SHA" "$(git rev-parse HEAD:vendor/lody)" -- . \
':!UPSTREAM.md' ':!BLITZ-PATCHES.md' | sort -u > "$ACTUAL_SEAM_FILES"
comm -23 "$ACTUAL_SEAM_FILES" "$DECLARED_SEAM_FILES" \
> "$UNDECLARED_SEAM_FILES"
if [ -s "$UNDECLARED_SEAM_FILES" ]; then
sed 's/^/undeclared vendor divergence: /' "$UNDECLARED_SEAM_FILES" >&2
exit 1
fiFor every retained seam, refresh the upstream anchor and line number in
BLITZ-PATCHES.md. Run its conflict drill and delete any seam whose upstream
replacement has landed. Daemon seams 19-21 cover the ACP-authentication queue,
the optional built-in MCP server, and host-selected cgroup parent/capacity
policy. Their environment options retain upstream defaults when absent; the box
service selects the host behavior. The ACP rule moves only an authentication
start to acp-auth:<configId>; submit and cancel remain on the default chain.
Renderer seams 22 and 23 add the sidebar footer slot and host side-panel tabs.
The browser-panel seam numbered 20 on main was withdrawn before the merge.
The ambient-IPC audit is not a formatting gate. Any newly reachable unbound IPC
site reported by lody-ipc-client-isolation.test.ts is a class-C decision.
The seam tests read checked-in pristine upstream sources because shallow CI
clones may not contain the upstream commit object. Refresh all eight from a
clone that contains NEW_SHA:
for file in sessions/session-tab-bar sessions/session-detail \
sessions/session-side-panel-tab-bar \
mobile/mobile-session-tab-sheet mobile/mobile-home-screen; do
git show "$NEW_SHA:packages/components/src/components/$file.tsx" \
> "packages/webapp/test/upstream-baseline/$(basename "$file").tsx.txt"
done
git show "$NEW_SHA:apps/cli/src/lib/message-processor.ts" \
> packages/webapp/test/upstream-baseline/message-processor.ts.txt
git show "$NEW_SHA:apps/cli/src/agent/agent-client.ts" \
> packages/webapp/test/upstream-baseline/agent-client.ts.txt
git show "$NEW_SHA:apps/cli/src/mcp/lody-mcp-http-server.ts" \
> packages/webapp/test/upstream-baseline/lody-mcp-http-server.ts.txtUpdate the provenance SHA in
packages/webapp/test/upstream-baseline/README.md, then move each line-number
anchor in packages/webapp/test/lody-seam-pin.test.ts and
vendor/lody/BLITZ-PATCHES.md to the corresponding line in the new pristine
source. Never weaken a test merely because an anchor moved.
Upstream resolves through its pnpm catalog; the Blitz webapp resolves the
renderer dependencies through packages/webapp/package.json. Review both the
catalog and upstream patch set.
git diff "$OLD_SHA" "$NEW_SHA" -- \
pnpm-workspace.yaml packages/components/package.json patches/
grep -n 'patchedDependencies' -A 30 vendor/lody/pnpm-workspace.yaml
node scripts/apply-vendor-patches.mjsMove every renderer dependency that Blitz names to the upstream catalog value.
Remove a deleted upstream patch from scripts/apply-vendor-patches.mjs; there
is no second patch copy. Recheck the guarded @pierre/diffs side-effects fix
there and delete it if upstream's published package now names the shipped
dist/components/web-components.js or drops sideEffects.
Review the workaround inventory under “Things upstream does not support” in
BLITZ-PATCHES.md. In particular, delete a mirror when upstream:
- passes
machineFlockRowson the archive worktree-cleanup path; - accepts a local project's own remote without a cloud repository row; or
- lets the local runtime supply the machine list for ACP capability refresh.
Update every moved anchor even when the workaround remains.
The CLI workspace uses five adapter gitlinks: core, Claude, Codex, DSH, and
Grok. Kimi remains a gitlink but is excluded by
vendor/lody/pnpm-workspace.yaml; do not materialize it for this build.
Sync, review, stage, and check the reviewed trees:
npm run lody:adapters:sync
git diff -- vendor/lody-adapters
git add vendor/lody-adapters
npm run lody:adapters:check
npm run lody:adapters:check -- --fetchThe sync fetches each exact public gitlink commit, archives only its tracked
tree, and removes dist/ and node_modules/. It then replaces the matching
vendor/lody-adapters/<name>/ snapshot. Tests, docs, and lockfiles remain.
Sync reports the old and new SHAs plus file write and removal counts. It exits
without comparing its output to the old index.
The first check uses no network. It proves all five stamps match their gitlinks
and .gitmodules. It also checks package presence and rejects unstaged snapshot
changes. The fetch check exports every exact upstream commit and compares its
bytes and modes with the staged snapshot. Record all six gitlink SHAs and any
adapter-tree changes in the PR body.
Build the same package the image installs. Every retained daemon seam already lives in the subtree; the builder never rewrites the compiled output.
LODY_OUT=$(mktemp -d)
npm run lody:build -- --out "$LODY_OUT"
mapfile -t TARBALLS < <(find "$LODY_OUT" -maxdepth 1 -type f -name 'lody-*.tgz' -print)
test "${#TARBALLS[@]}" -eq 1
TARBALL=${TARBALLS[0]}
jq . "$LODY_OUT/BUILD.json"The script exports HEAD:vendor/lody, overlays the five reviewed adapters,
creates a Corepack shim for upstream's bare pnpm calls, performs the frozen
install and build, and copies the notice. It derives an npm lockfile v3
shrinkwrap from apps/cli production dependencies in pnpm-lock.yaml.
Every runtime package has the reviewed version, tarball URL, and integrity.
The script also checks the short required-asset manifest. Missing workers,
presets, WASM, the notice, the shrinkwrap, or the stamp fail the build.
Unrelated chunks are allowed. The identical stamp sits beside the tarball and
inside dist/BUILD.json. distSha256 covers sorted path and content-digest
records for dist, except the self-referential stamp. A measured warm build
took about 91 seconds. That is evidence, not a timeout.
For a Docker builder without Git, pass --source /path/to/lody. The script
reads source identity from that tree's UPSTREAM.md and adapter identity from
the reviewed vendor/lody-adapters stamps beside the script's repository root.
Extract the tarball into a temporary prefix. Run npm at the package root so it enforces the included shrinkwrap. Then point the harness at that directory:
mkdir -p "$LODY_OUT/install/lib/node_modules" "$LODY_OUT/unpack"
tar -xzf "$TARBALL" -C "$LODY_OUT/unpack"
mv "$LODY_OUT/unpack/package" "$LODY_OUT/install/lib/node_modules/lody"
npm ci --prefix "$LODY_OUT/install/lib/node_modules/lody" --omit=dev
export LODY_BUNDLE="$LODY_OUT/install/lib/node_modules/lody"
test -f "$LODY_BUNDLE/dist/index.js"Set LODY_CLAUDE_BINARY to the same Claude CLI used by the image when the
default /opt/blitz/npm/bin/claude does not exist. The post-sign-in suite names
that missing prerequisite when it skips. Run every non-probe harness consumer
serially:
cd packages/webapp
PAIR_TESTS=$(grep -rl --include='*.test.ts' --include='*.test.tsx' \
'lody-daemon-harness' test | grep -v '\.probe\.' | sort)
npx vitest run --maxWorkers=1 $PAIR_TESTS
cd ../..At HEAD that discovery selects these 15 suites:
lody-acp-authentication lody-archive-lifecycle
lody-attachments lody-post-signin-turn
lody-project-control-frames lody-rail-groups
lody-session-rail lody-session-roundtrip
lody-session-surface lody-session-workdir
lody-shared-endpoints lody-shared-surface
lody-sharing-relay lody-worktree-composer
lody-worktree-session
The discovery command, rather than this snapshot, is authoritative when a new
harness consumer lands. lody-session-surface, lody-session-rail, and
lody-post-signin-turn are the minimum smoke subset, not the whole gate.
Leave BLITZ_LODY_LIVE_TURN unset. Tests that dispatch a paid model turn skip
by design in lody-post-signin-turn, lody-session-rail,
lody-session-roundtrip, lody-session-surface, lody-shared-surface, and
lody-worktree-composer; their remaining cases must run. The
lody-keepalive-activation and lody-switch-cost probe suites in that directory
also stay out of PR CI; they require BLITZ_LODY_SWITCH_PROBE=1 and belong in a
scheduled/manual performance run.
Run the daemon-free bridge boundary gates too. They use stand-ins and are not evidence that the built daemon starts, but they protect the box boundary:
cd packages/box/guest-tests
npx vitest run --maxWorkers=1 \
test/lody-bridge-frames.test.ts \
test/lody-bridge-control-stream.test.ts \
test/lody-bridge-share.test.ts \
test/lody-projects-registration.test.ts
cd ../../..The required lody-daemon job runs this discovery against the artifact it just
built and fails explicitly if a non-paid suite reports a bundle-absence skip.
Before installing that artifact, it also runs the focused authentication-queue,
built-in-MCP request/reminder, and session-sandbox tests in the preserved
scratch tree that produced it.
The install fetches only the shrinkwrap's exact registry tarball URLs. npm
checks every integrity before extraction. Lifecycle scripts remain enabled for
native dependencies in the target runtime. The reviewed Lody workspace marks
better-sqlite3 13 as build-ignored because its package already carries
platform prebuilds. The pair gate proves those locked dependencies install and
the resulting daemon starts.
Validate these daemon-authored corpora against the just-built daemon:
packages/schema/fixtures/lody-data-plane/server/, except its two documented synthesized cases, retaining the derived chunked frame;packages/schema/fixtures/lody-project-registration/request/andresponse/;- the non-synthesized stream and envelope cases in
packages/schema/fixtures/lody-session-control-stream/; and packages/schema/fixtures/lody-share-claim/catalog-full.json, regeneratingcatalog-shared.jsonthrough the real bridge projection.
Do not churn real IDs on every merge. Re-capture only when reviewed protocol
behavior changes, and normalize only the nondeterministic IDs and timestamps
each README already documents. Until scripts/lody-validate-fixtures.mjs
lands, drive the capture scenarios with the pair harness and run every listed
conformance test. After it lands, validation uses the PR-built artifact and is
mandatory.
Every recaptured corpus README must contain this exact provenance form with real values:
Captured from the daemon built from `vendor/lody` at `<upstreamSha>` (`distSha256` `<sha>`).
The qualified README paragraphs naming the old npm daemon in the captured corpora are historical capture facts, not a daemon-selection rule.
Run the focused seam pins first, then the repository and Go gates:
cd packages/webapp
npx vitest run --maxWorkers=1 \
test/lody-seam-pin.test.ts \
test/lody-v1-scope-sources.test.ts \
test/lody-ipc-client-isolation.test.ts
cd ../..
npm run typecheck
npm run lint:gate
npm test
( cd packages/box/credential-helper && go test ./... )
( cd packages/box/gateway && test -z "$(gofmt -l .)" && go test ./... )
git diff --checklint:gate must pass; never raise lint-baseline.json to absorb upstream
findings. Once present, also require the adapter-drift, pin-provenance,
dist-manifest, notices, source-seam behavior, fixture-validation, built-image
smoke, /lody/build contract, pair-matrix, and canary-image-input gates named
in plans/LODY-DAEMON-FROM-TREE.md.
Automation must stop for:
- a class-C seam conflict;
- upstream behavior that is mechanically valid but unwanted in BlitzOS;
- a red or unexpectedly skipped non-paid pair gate;
- a new ambient IPC site reported by the source audit;
- a fixture semantic change that requires a reviewed recapture;
- the final PR review and merge click.
Everything else is routine evidence gathering and may be automated.
Until plan PR E opens the PR itself:
git push -u origin "$(git branch --show-current)"
gh pr create --title "chore(vendor): merge Lody ${NEW_SHA:0:12}" --body-file <file>The PR body must include:
- old and new upstream SHAs, ref, commit list/count, and whether it is a release;
- all six adapter gitlink SHAs and adapter-tree changes;
- every seam file upstream touched and its A/B/C resolution;
- deleted seams, retained workarounds, and new ambient IPC findings;
- tarball and
BUILD.jsonidentity plus package-output review; - fixture validation or reviewed recapture details;
- exact gate results, including paid/probe skips; and
- canary transition impact.
Do not merge it. A person reviews the upstream behavior and clicks merge.
Every push to main runs the image and payload jobs in
.github/workflows/canary.yml. The image job derives a SHA-256 release ID from
the Git object IDs of the base-owned inputs listed in
packages/control-plane/scripts/lib/box-image-inputs.mjs. vendor/lody,
vendor/lody-adapters and the Lody build scripts are NOT base inputs: they
feed the Dockerfile's lody-build and daemon stages, and the payload version
hashes the resulting daemon.tar.gz. A base-input change builds an amd64
image with Lody sessions enabled, boot-smokes it, publishes parts before the
manifest under immutable box-image/<releaseId>/ keys, passes the exact
ref/tag/archive digest to deploy, and verifies both commit and image tag
through /version. The payload job then publishes or reuses
box-payload/<version>/ and the deploy pins it.
box-image-key.test.mjs parses build-context COPY instructions and rejects
a source that is neither a base input nor a payload input. It also proves a
Lody-only change keeps the base release ID.
A vendor-only merge therefore reuses the current base image and publishes a
new payload. Every running box applies the new daemon in place at an idle
boundary (docs/BOX-IMAGE.md, plans/THIN-IMAGE.md), without a follow-up
source commit. Plan PR D adds /lody/build so the browser can compare the
package's own stamp with its renderer commit.
New boxes use the newly deployed pin. Existing cloud boxes replace their
container only after updateRequested is set through
packages/control-plane/core/box-config.ts. An upstream merge never silently
kills every running field box.
| Document | Scope |
|---|---|
docs/LODY-MERGE.md |
The one current upstream-merge procedure |
vendor/lody/BLITZ-PATCHES.md |
Declared seams, upstream anchors, conflict drills, and workaround inventory |
vendor/lody/UPSTREAM.md |
The upstream revision, subtree squash commit, and adapter gitlink pins |
plans/LODY-DAEMON-FROM-TREE.md |
Approved source-built-daemon design, gates, and migration PRs |
plans/LODY-SESSIONS.md |
Historical design rationale; its old merge and npm-pin rules are superseded |