Merge pingdotgg/t3code main into coder-only. Follow the steps in order. Read AGENTS.md
and the upstream seams in coder-only.md first; that list is the
only fork behavior to keep. Commands run from the repository root of the sync worktree.
git fetch origin && git fetch upstream main. Do not moveorigin/main; mirroring it is a separate step the owner authorizes.- Choose the exact upstream commit and read its range. Never take "the tip" without reading it:
git log --oneline origin/coder-only..upstream/main. Compare the range with the open issues labelledupstream(gh issue list --label upstream): note any the range fixes, and the fork seams that fix makes redundant. - Set the variables every later step uses:
SHA=<verified upstream sha> PREV=$(git rev-parse origin/coder-only) MB=$(git merge-base "$PREV" "$SHA")
$MBis the previous sync point. The seams doc's "checked against" lines name the upstream commit each carried fix was last compared with. - Create a worktree branch from
origin/coder-only(in T3 Code, use a worktree workspace strategy):git worktree add -b t3code/sync-upstream-<short sha> <path> origin/coder-only. There, runpnpm install --frozen-lockfile.
git -c merge.directoryRenames=false merge --no-ff --no-commit "$SHA". Without the setting, git infers directory renames from earlier upstream moves and files mobile adds underapps/mobile/src/libland inpackages/shared/srcas stage-3-only "added" files that upstream never had there (six in the 5cbe82a870 sync). Check any such file withgit ls-tree "$SHA" -- <path>: an empty answer means git invented the path.- Take upstream's version of a file and reapply only the seams the seams doc lists, each marked
Coder:in code. A difference not listed there is drift: remove it. - Leave upstream bugs unfixed unless they break an
AGENTS.mdboundary. - If adopting upstream would remove fork behavior the seams doc lists, stop and tell the user.
- To reopen a file resolved wrongly with its conflict markers:
git checkout -m -- <file>. - Resolve test files block by block, as code. Taking the fork's whole test file drops upstream's mechanical edits (renamed types, branded values) from conflicted regions only, which typecheck then reports far from their cause. Where diff3 interleaves two versions of one test, replace that whole test with one side rather than splicing hunks.
- Upstream test files that merge in without conflict can still exercise a cut surface or an SDK
hook the CLI transport lacks (for example the SDK's
spawnClaudeCodeProcess); review every added or modified*.test.*in the range and adapt or cut it with aCoder:comment. - When upstream edits a block the fork moved into a Coder file, apply the edit there; the marker and lost-line checks in section 6 do not catch this.
- An upstream commit for a cut surface can also add hooks to carried files that only the cut
surface calls (in the dd5c7bd09e sync,
ProviderUpdatesAction'squietoption and returned runs for "Update everything"). Leave those hunks out, mark the fileCoder:, and list the seam. - Fork-only tests that build upstream services (
mcp/bridge/T3ToolBridge.test.ts,storageCleanup.coder.test.ts,ws.projects.test.ts) fail typecheck when upstream adds a service dependency; mock it as upstream's own tests of that service do. Their fixtures cast partial records (as unknown as), so a field upstream starts reading fails only at runtime (2a7bd1673c: thread shells'lineageandsettledAt); copy the fields from upstream's own fixture for that service. - When upstream rewrites a file wholesale (in 2a7bd1673c,
ChatView.tsxandChatComposer.tsxmoved fromuseCallbackto plain functions anduseStableCallback), take upstream's file and replay the fork's delta hunk by hunk:git diff "$MB" "$PREV" -- <file>, split it into one file per hunk, and apply each withpatch -p1 --fuzz=3 -i <hunk>(patchreads a positional argument as the file to patch, so pass the hunk with-i). Apply rejects by hand, and check that no fuzzy hunk landed in the wrong block (in that sync two retry buttons swapped theirimageandfilevariables). Then confirm the delta survived: every line the fork added in$MB..$PREVis in the result, and every line the fork removed that upstream still has is not:macOSpython3 - "$MB" "$PREV" "$SHA" <file> <<'PY' import subprocess, sys; from collections import Counter f = sys.argv[4] rev = lambda r: Counter(l.strip() for l in subprocess.run(["git", "show", f"{r}:{f}"], capture_output=True, text=True).stdout.splitlines() if len(l.strip()) > 3) mb, prev, sha = (rev(r) for r in sys.argv[1:4]) res = Counter(l.strip() for l in open(f).read().splitlines() if len(l.strip()) > 3) for l, n in prev.items(): if n > mb[l] and res[l] < min(sha[l] + n - mb[l], n): print("lost fork line:", l) for l, n in mb.items(): if n > prev[l] and sha[l] and res[l] > max(sha[l] - (n - prev[l]), 0): print("fork cut back:", l) PY
patchreverses a hunk it takes for "already applied" without failing; pass-Nso such hunks become rejects to apply by hand. Expect hook dependency-list entries and call sites the refactor rewrote; read every other line, since it is a lost or reverted seam (that sync's check caught a "pull request" wording regression this way). - An upstream commit that only deletes comments (cfafb4bb54's #19559 touched about 3,100
files) conflicts wherever the fork edited nearby. Merge it in its own stage: merge up to the
commit's parent first, then the commit with the rest of the range. With
-c merge.conflictStyle=diff3, a hunk whose upstream side differs from the base only in comment lines takes the fork's side with upstream's comment edits applied. Where upstream deleted a comment that also held aCoder:note, keep the note as its own comment. Edit comment blocks whole: removing upstream's lines one at a time can drop a/**or*/the fork's text still needs (typecheck catches the orphaned lines). Afterwards, list comment lines upstream deleted that the result still has, since an auto-merged hunk can keep them too. - A mechanical upstream refactor (e.g. e85539ab59's traced
Effect.fnwrappers, #21989–#22000) re-indents whole functions, so conflicts span the fork's rewrites. Merge it in its own stage. For a file the refactor alone touched, take upstream's file and replay the fork's delta (git diff "$MB" "$PREV" -- <file>) withpatch -p1 -l --fuzz=3, which ignores the indentation change, thenvp fmt. Where the fork replaced a function wholesale (Kilo Code, Codex, and Claude drivers'create), keep the fork's body and apply only upstream's wrapper. Effect.fn spans export nothing here, but matching upstream's shape keeps later syncs small. - When upstream splits a file into new modules (688ee52f0e: ChatView, ChatMarkdown, ChatComposer,
MessagesTimeline, CommandPalette, SettingsPanels, the sidebar, and the pull requests page), the
new modules arrive as plain added files with none of the fork's delta, and can carry cut
surfaces again (
useSystemComposerBanners' server-update notice). Replay the old file's delta into whichever module now holds each block, strip cut surfaces there, and run the lost-line check above against the old file plus its new modules together. Section 6's marker check then shows the old file's drop and the new modules' gain; the totals should match. - When upstream moves web logic into a shared package (07d38b8d17:
LinkPullRequestDialog's parsing moved to client-runtime'spullRequestLinkInput.ts), move the fork's test adaptations with it, keep the moved module upstream's apart from wording, and keep a seam that needs web code (gitLabMergeRequestBrowserUrl) as a wrapper at the web caller. - An upstream sweep that deletes code nothing upstream reaches (67661cfbb5's #26238) also removes
helpers, mock-agent options, and guards that only fork-only tests (tests the fork kept after
upstream deleted its copy) or fork modules still use. Typecheck misses most of these, since
the tests mock by name or spawn scripts; only the full suite finds them (that sync:
GitWorkflowService.test.ts'sjjguard,useThreadActionMenu.test.tsx's entity mocks, the Kilo adapter test'sacp-mock-agent.tsoption). Keep a piece a fork module needs as aCoder:seam; adapt a fork-only test to the remaining behavior. - Upstream code the fork carries can start importing a module the fork deleted (in 2a7bd1673c,
ThreadManagementService.tsimportsenvironment/ServerEnvironment.tsforServerEnvironmentIdentity). Declare the piece it needs in the matching Coder module (coderEnvironment.ts) and point the import there with aCoder:comment. - An upstream rewrite can add a new caller of the upstream function a seam replaces, in a file that
merged cleanly (6d450a4284's #25869:
ChatView.tsx's new-thread permissions label calledresolveCompatibleRuntimeModewhile the composer used the fork's resolver). After resolving,rgthe tree for the upstream names each touched seam replaces, not only the conflicted files. - Fork wording changes inside upstream code (GitLab's "merge request !N") break the new test cases
upstream adds for that code (48a292364a's #26395).
pnpm test:coderstops at the first failing package, so read its log for the failing package and rerun until every package has run.
Upstream files under directories the fork deleted stay deleted, whether upstream modified or added
them. After git merge stops, run node scripts/sync-resolve-deleted.mjs (or
pnpm sync:resolve-deleted when no package.json or pnpm-lock.yaml is conflicted, since pnpm
checks dependencies before running a script). It git rms each modify/delete conflict under the
paths listed in the script and prints every other conflict and every upstream-added file under
those paths, untouched, for you to decide. It never resolves a content conflict. A modify/delete it leaves for a human on a surface the fork
already removed belongs in the script: git rm it and add the path to DELETED_PATHS in the same
stage (6d450a4284 added 56). The files it lists are untouched but staged, so removing
one needs git rm -f. To remove every listed upstream-added file at once:
node scripts/sync-resolve-deleted.mjs | awk '/^Added by upstream/ { on = 1; next } on && /^ / { print $1 }' > /tmp/sync-added.txt
git rm -q -f --pathspec-from-file=/tmp/sync-added.txtThe script lists only paths under deleted directories. Upstream also adds files for cut surfaces
in kept directories (in the 5cbe82a870 sync: observability/OtlpEndpointCheck.ts,
settings/TelemetryExportSettings.tsx, provider/antigravityUsageLimits.ts, and provider-pi's
usage.ts; in the 2a7bd1673c sync: server outboundHttp.ts and zipArchive.ts, the OTLP
config.test.ts, web sidebar/SidebarUpdateCapsule.tsx and settings/DatabaseStorageNotice.tsx,
and client-runtime state/threadTranscriptHttp.ts, which the fork replaced with an RPC); review
git diff --name-only --diff-filter=A "$MB" "$SHA" -- apps packages for them. Upstream's manual
repro fixtures and seed scripts under scripts (eb1510d249: fixtures/issue-16534; bb7783e295:
fixtures/issue-18188, fixtures/issue-21397.json, seed-issue-21397.py and its test) are
upstream tooling; delete them. Upstream's
patches for mobile or desktop packages land in patches/ (2a7bd1673c: @expo__ui@58.0.18.patch);
delete any that pnpm-workspace.yaml's patchedDependencies does not list. An upstream bump
of a patched package the fork cut renames its patch, so the new file arrives as an addition
outside any deleted path (67661cfbb5: alchemy@2.0.0-beta.84.patch); compare ls patches with
patchedDependencies after every stage. A fork-deleted file
outside those paths that upstream modified (a cut
surface's test or settings panel) is listed as "deleted by fork"; keep it deleted unless the seams
doc says otherwise. An upstream rename out of a deleted directory into a kept one (in this batch,
mobile's microphonePriority.ts into packages/shared) arrives as an added file with only stage
3; carry it only if a carried surface uses it. The categories
(top-level directories in brackets):
- Auth and pairing (
apps/server/src/auth, webcomponents/auth, client-runtimeauthorization). - Preview and browser (
apps/server/src/preview, webbrowser,components/preview). - Device (
apps/server/src/device, webcomponents/device, client-runtimedevice). - HTTP routes (
http.tsroute files other than the server's signed asset route inapps/server/src/http.ts, and the uncarriedassets/modules). - Forgejo, GitHub, Azure DevOps, Bitbucket, and GitCafe (
packages/source-control-*except core, gitlab, and testing). - Other provider packages (
packages/provider-{cursor,fx,grok,muse,opencode},apps/server/src/provider/acp) and their orchestrator replay fixtures (matched by pattern; upstream's new entries for them intestkit/fixtures/index.tsstill merge in, and typecheck reports their imports of the deleted files). - ACP Registry surfaces Kilo Code does not use (provider-acp-registry's
auth.ts,authenticationState.ts,copilotAcp.ts,devinAcp.ts, andtesting/; provider-acp'snativeLogging.ts). - Desktop (
apps/desktop,native,packaging,assets); mobile (apps/mobile); hosted web (apps/marketing,apps/server/src/cloud, webcloud,components/clerk). - Triage, dev servers, and dev replay (
apps/server/src/triage,apps/server/scripts/dev-replay*, webDevReplayHost.tsx,chat/ThreadServersPanel.tsx,chat/TriageDraftHero.tsx,hooks/useStartTriage.ts,lib/triageTarget.ts,docs/user/triage.md). - Relay (
apps/server/src/relay,infra,packages/ssh,packages/tailscale); telemetry (apps/server/src/{telemetry,resourceTelemetry,usage,diagnostics}, webcomponents/usage). - Upstream repository tooling (
.github,.repos,oxlint-plugin-t3code,docs/operations,apps/server/integration,evals,.coderabbit.config.ts) and upstream's CI-only checks (AgentContextBudget.test.ts,verify-built-boot.ts).
Re-derive the list, and update the script's data, when the fork cuts or restores a surface:
git -c diff.renameLimit=0 diff --no-renames --name-status "$MB" origin/coder-only |
awk '$1 == "D" { split($2, p, "/"); print (p[1] == "apps" || p[1] == "packages") ? p[1] "/" p[2] : p[1] }' |
sort | uniq -c | sort -rnapps/server/src/server.ts: upstream's file minus the cut surfaces. Upstream's edits to the carried declarations merge; keep theirCoder:changes and the export block. Conflicts sit in regions the fork deletes (layerMakeServer,layerRuntimeCoreDependencies*, HTTP, auth, preview, and telemetry layers): keep the deletion, and port a new service or provide the helper needs intocoderServer.ts's matching composition (CoderRuntimeCoreLive,CoderRuntimeDependenciesLive,CoderRuntimeStartupLive, ormakeCoderRuntimeLayer). Export a carried declaration only whencoderServer.tsuses it. A service that gains a requirement (758d773f57:ProviderRegistryreadingProviderLatestVersions) still typechecks inapps/server, because only the helper entry point launches the composed layer; runpnpm typecheck:helperto catch a requirementcoderServer.tsdoes not provide.apps/server/src/ws.ts: upstream's file minus the cut surfaces, with in-placeCoder:changes; the fork-only methods live incoderWs.ts, merged in at thereturnhook. Keep upstream's handler and helper order and take upstream's handler edits. Its handler object is typed byCoderWsRpcGroupminusCODER_WS_METHODS, so typecheck fails until every served upstream method has a handler there (see section 5 for adding RPCs).coderWs.tswraps upstream's Files list, read, and write handlers rather than copying them, so their upstream edits apply as they merge.apps/web/src/components/ChatView.tsx: the fork removes preview, device, usage, self-update, local editor, and feedback code. Take upstream's hunk, then re-delete the hole and keep itsCoder:comment.docs/user/*.md: take upstream's text, then reapply "merge request", "MR",!N, "T3 Coder".knip.jsonc: keep ours; add or dropignoreIssuesentries aspnpm knip:checkreports, and remove anyworkspacesentry upstream adds for a package the fork does not carry (2a7bd1673c:packages/source-control-github). Its long entries stay on one line; do not reformat the file.package.jsonexport maps: when both sides add an entry near the same place, the merge can keep it twice. JSON parsing silently keeps the last copy, so check changed manifests for duplicate keys (vp fmtdrops them, as does ajson.load(..., object_pairs_hook=...)check).- Patch files under
patches/: when upstream bumps a patched package, the fork's patch for the old version arrives as modify/delete. Check whether the new version still needs the fork's hunks, and append them to upstream's new patch by hand;pnpm patch-commitrewrites the whole file in its own order. pnpm checks each hunk header's line counts when it applies a patch, so an appended hunk must keep exact@@ -a,b +c,d @@counts;pnpm install --frozen-lockfilefails on a miscounted one. pnpm-lock.yaml: keep ours, runpnpm install --no-frozen-lockfileto apply the mergedpackage.jsonfiles, then confirmpnpm install --frozen-lockfilepasses. Do this even when the lockfile merged cleanly: the auto-merge carries importer entries for deleted packages, and any laterpnpm exec/vprun silently rewrites the lockfile when it disagrees with apackage.json. Start from ours withgit show HEAD:pnpm-lock.yaml > pnpm-lock.yaml(during a merge,git checkout --oursworks only on a conflicted path). Resolve the lockfile and everypackage.jsonbefore running anypnpm <script>, thetypecheck:*scripts included: pnpm checks dependencies first and installs when a manifest disagrees with the lockfile. Re-resolving from ours picks the newest version of each package new to the lockfile, while upstream's lockfile andthird-party-licenses.config.jsonoverrides name the version upstream resolved (7df7cb9999:@nodable/entities2.2.0 against upstream's 2.1.1 failed the web build's license step). List the newly added package versions that differ from upstream's lockfile and replace theirpackages:andsnapshots:entries and references with upstream's, then confirmpnpm install --frozen-lockfilepasses.- When upstream moves a dependency line within a
package.json(67661cfbb5:@opencode-ai/sdkfromdependenciestodevDependenciesinapps/server), the merge can add it back for a package the fork cut. Reviewgit diff HEAD -- '*package.json'after resolving manifests. - New upstream code can import a dependency the fork had removed with an old cut (7df7cb9999:
yauzlfor document text,jszipfor office previews). Typecheck reports the missing module; restore upstream's dependency entry rather than adapting the code. apps/coder-gateway/src/server.ts's CSP pins the hash ofapps/web/index.html's inline boot script. When upstream edits that script (comment pruning counts), recompute the hash;server.test.tsfails until then.- A package that gains a dependency on deleted host packages (this batch: client-runtime on every
source-control-*client definition) keeps onlysource-control-coreand-gitlab.
- Claude CLI transport. Every option upstream passes to
query()must be inClaudeAgentSdk.ts'sSUPPORTED_QUERY_OPTIONS, and everyQuerymethod it calls in the seams doc's mapped list; an unsupported option fails every Claude turn at runtime only. ReadmakeClaudeQueryOptionsand thequery({call:git diff "$MB" "$SHA" -- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts apps/server/src/provider/ClaudeProvider.ts. NewQuerycalls, by method name, since upstream's receivers vary (q,queryRuntime,liveQuery.query):SDK bumps:sdk=$(ls -d node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@*/node_modules/@anthropic-ai/claude-agent-sdk | head -1) methods=$(awk '/^export declare interface Query /,/^}/' "$sdk/sdk.d.ts" | grep -oE '^ [a-zA-Z]+\(' | tr -d ' (' | sort -u | paste -sd'|' -) git diff "$MB" "$SHA" -- apps/server/src ':!*.test.*' | grep -E "^\+.*\.($methods)\("
git diff "$MB" "$SHA" -- '*package.json' | grep claude-agent-sdk. Upstream may add an SDK hook in the runner's ownquery({call rather than inmakeClaudeQueryOptions(spawnClaudeCodeProcessin dd5c7bd09e);ClaudeCli.tsmust honour it, and upstream'sClaudeAgentSdkQueryRunner.test.tsfake-CLI tests then exercise it. A rejectedQuerymethod is loud only if upstream lets the rejection surface: upstream wraps some calls (setMcpServersinlayerQueryRunner) so a failure is logged and the turn continues without the feature. For every new call, read how upstream handles its failure; implement the method inClaudeCli.tsfrom the SDK'ssdk.mjsrequest shape (findasync <method>(innode_modules/.pnpm/@anthropic-ai+claude-agent-sdk*/node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs) and add aClaudeCli.test.tscase whose fake CLI answers only the exact shape. - Node 24 runtime. The laptop and the workspaces run Node 24, while upstream develops on
Node 26 (
engines.nodepinned to26.12.1since ed3f965e07). Keep rootengines.nodeat the fork's^24.13.1and the@types/nodecatalog entry on major 24 even when upstream bumps them, so typecheck rejects a Node 25 or 26-only API in carried code. If upstream code needs one, stop and tell the user. Bumps:git diff "$MB" "$SHA" -- package.json pnpm-workspace.yaml | grep -E 'engines|"node"|@types/node'. - MCP servers. T3 tools reach agents over the file bridge, never an MCP server, because work
Claude Code and Codex policies block custom MCP servers (
AGENTS.mdboundary 6). Carry a new upstream toolkit over the bridge and cut any new path that hands a provider T3's MCP endpoint. Listeners inside the workspace are otherwise allowed. Search the range:git diff "$MB" "$SHA" -- apps/server/src packages ':!*.test.*' | rg '^\+.*(mcp_servers|mcpServers|McpHttpServer)'. - Capabilities.
coderEnvironment.test.tsfails when upstream adds anExecutionEnvironmentCapabilitieskey; decide it (advertise, or add toCODER_OMITTED_CAPABILITIESwith a reason). - RPCs. List RPCs upstream added to
WsRpcGroupin the range; serve each inCoderWsRpcGroupor record why it is omitted (the pattern needs digits for theWsOrchestrationV2…Rpcnames):rpcs() { git show "$1:packages/contracts/src/rpc.ts" | sed -n '/^export const WsRpcGroup = /,/^);/p' | grep -oE 'Ws[A-Za-z0-9]+Rpc' | sort -u; } comm -13 <(rpcs "$MB") <(rpcs "$SHA")
- Shared contracts. Never drop a field the fork does not use. For each contract file upstream
changed (
git diff --name-only "$MB" "$SHA" -- packages/contracts/src), every hunk ofgit diff "$SHA" -- <file>must be a listed Contracts seam. - Always-grant scopes.
useEnvironmentScope,useEnvironmentsWithScope,readEnvironmentScope(apps/web/src/state/session.ts),useFilesystemReadAccess, and client-runtime's command permissions still grant everything. New gates:git diff "$MB" "$SHA" -- apps/web/src packages/client-runtime/src | grep -E '^\+.*(EnvironmentScope|EnvironmentsWithScope|FilesystemReadAccess|commandPermissions|RpcPermissionGuard)'. - Wording. New user-facing strings follow the merge-request wording seam. Use
rg(some shells aliasgrepto tools that reject\b), and include the source-control packages and#${references, which the!123seam replaces:git diff "$MB" "$SHA" -- apps/web/src packages/client-runtime/src packages/shared/src apps/server/src docs/user packages/source-control-core/src packages/source-control-gitlab/src ':!*.test.*' | rg '^\+.*([Pp]ull [Rr]equest|\bPRs?\b|T3 Code\b|#\$\{)'. Upstream tests that assert merge-request summaries (runtimeLayer.test.ts's watch tests) spell them#N; the fork's text is!N. New text that describes a mechanism the fork cut (in dd5c7bd09e, provider update checks "fetch model catalog updates", where the catalog is bundled) keeps describing what T3 Coder does. - Source-control host definitions.
source-control-definitions'registry.tsand the server'ssourceControl/sourceControlDefinitions.tsregister only GitLab. When upstream moves host behavior into definitions or driver hooks, delete the fork's per-kind GitLab shims that the GitLab-only registry now expresses, and move a remaining GitLab difference into GitLab's definition when the field exists for it (be1d066732:referenceAutolinksreplaced the web's GitLab autolink code). When upstream deletes code in favor of a hook, check that the helper wires the hook: be1d066732 replacedPullRequestService's unknown-host refinement with the repository identity resolver'srefine, which an older sync had dropped from the fork, so self-hosted GitLab detection would have silently stopped. Upstream tests that build GitHub, GitCafe, or Azure URLs fail only at runtime once the registry no longer parses them; move them to GitLab URLs or cut them. A host's media, settings, and credential helpers that only cut hosts use go inknip.jsonc'signoreIssues.
Before committing the merge, compare each upstream-modified file's Coder: marker count with
$PREV; a drop must be inside a block upstream deleted, or be a seam that upstream's own
mechanism now expresses (name it in the merge commit body).
git diff --name-only "$MB" "$SHA" | while IFS= read -r f; do [ -f "$f" ] || continue
a=$(git show "$PREV:$f" 2>/dev/null | grep -c 'Coder:'); b=$(grep -c 'Coder:' "$f")
[ "$a" = "$b" ] || echo "$f: $a -> $b"; doneThen list lines upstream added in $MB..$SHA that the result lacks; each must be a listed seam or
a deleted surface. Use the merged range, never upstream/main's tip (later work shows as losses).
git diff --name-only --diff-filter=AM "$MB" "$SHA" -- apps packages ':!*.test.*' | while IFS= read -r f; do
[ -f "$f" ] || continue
git diff -U0 "$MB" "$SHA" -- "$f" | sed -n 's/^+[[:space:]]*//p' | grep -v '^++ ' | awk 'length > 3' |
while IFS= read -r line; do grep -qF -- "$line" "$f" || printf '%s: %s\n' "$f" "$line"; done
doneThen list lines that both $PREV and $SHA have but the result lacks: neither side deleted them,
so each is a merge loss unless the line moved (7df7cb9999 lost projectScriptEditor.tsx's
run-on-settle switch this way; the check above misses it because the line predates $MB):
python3 - "$PREV" "$SHA" <<'PY'
import subprocess, sys; from collections import Counter
prev, sha = sys.argv[1:3]
show = lambda r, f: subprocess.run(["git", "show", f"{r}:{f}"], capture_output=True, text=True)
lines = lambda s: Counter(l.strip() for l in s.splitlines() if len(l.strip()) > 3)
files = subprocess.run(["git", "diff", "--name-only", "--diff-filter=AM", prev, "--", "apps", "packages"], capture_output=True, text=True).stdout.split()
for f in files:
a, b = show(prev, f), show(sha, f)
if a.returncode or b.returncode: continue
try: res = lines(open(f).read())
except FileNotFoundError: continue
pa, pb = lines(a.stdout), lines(b.stdout)
for l, n in pa.items():
if res[l] < min(n, pb[l]): print(f"{f}: {l}")
PYCheck that no patch backup or reject file was staged:
git ls-files | grep -E '\.(orig|rej)$' must print nothing (a BranchToolbar.tsx.orig slipped
into the fb990765a4 merge).
Finish with pnpm knip:check. Upstream's test pruning can leave an export used only by a cut
surface (mobile, relay, other providers) once the test that imported it is gone; list that file
in knip.jsonc's ignoreIssues, and delete the export instead when it is in a fork-only file.
Knip does not see unused imports and locals. Cutting a surface, or replaying the fork's delta into upstream's split modules, leaves them behind (688ee52f0e left about 50, including a dead Claude worker). List the ones the fork caused; an unused name that upstream's own file also has is upstream's, so leave it:
for p in apps/web apps/server apps/coder-gateway $(ls -d packages/*/ | sed 's#/$##'); do
[ -f "$p/tsconfig.json" ] && pnpm exec tsc --noEmit -p "$p/tsconfig.json" --noUnusedLocals 2>&1
done | grep -E 'TS61(33|92|96)' | sort -u | python3 -c '
import re, subprocess, sys
sha = sys.argv[1]
for line in sys.stdin:
m = re.match(r"(\S+)\((\d+),\d+\): error TS\d+: (?:.(\w+).|All imports)", line)
f, n, name = m.group(1), int(m.group(2)), m.group(3)
text = open(f).read()
if name is None:
name = re.findall(r"\w+", text.split("\n")[n - 1].split("{")[-1])[0]
up = subprocess.run(["git", "show", f"{sha}:{f}"], capture_output=True, text=True)
count = lambda s: len(re.findall(rf"\b{name}\b", s))
if up.returncode != 0 or count(up.stdout) != count(text):
print(line.strip())
' "$SHA"Remove each listed name, then rerun until it prints nothing: removing a declaration can leave its imports unused in turn.
In this order, fixing each failure before the next: pnpm test:coder; every typecheck listed in
AGENTS.md; pnpm knip:check; pnpm build; then
pnpm exec vp fmt --check $(git diff --name-only "$PREV" HEAD -- '*.ts' '*.tsx' '*.mjs' '*.md')
over the files the merge changed, since a comment deletion can leave a doubled blank line that
typecheck and tests never see (cfafb4bb54: ModelManifest.test.ts). Typecheck before the full suite finds most merge
breakage faster. For one file, pnpm exec vp test run --config vite.coder.config.ts <file>
covers apps/server only; run web, client-runtime, and package tests from the package
directory with pnpm exec vp test run <file>.
Then start up a real instance with mocks and do a simple test.
pnpm test:coder exits 0; a failure is real. It stops at the first failing package script, so
fix and rerun until it completes. A full run takes over ten minutes, longer than an agent's
foreground command limit, so run it in the background and read its log. Known results:
- The suite needs a Node version in the root
engines.node(the fork's^24.13.1). On an older Node, three upstream tests fail on unmodified upstream code: ProjectionRecovery's "selects unfinished recovery work without reading settled thread histories" (ProjectionStoreSetupError, "malformed JSON") and nodeSqliteClient's "keeps large text and blob parameters off cached statements" (node:sqlite binds an ArrayBuffer parameter as NULL) and "checkpoints the WAL on the worker instead of the writer" (its SQLite predates 3.51). Checknode -p process.versionagainstengines.nodebefore blaming the merge; do not skip them. apps/server/src/serverSettings.test.ts"follows a settings link that is repointed to another directory" and "reloads when a dangling settings link gets its destination" can hit their 2-second watcher timeout under full-suite load; #236 lists the other known flakes. Rerun it alone:pnpm exec vp test run --config vite.coder.config.ts apps/server/src/serverSettings.test.ts.- On macOS, upstream's
ClaudeSkills.test.ts"merges plugin enablement and selects installs in native registry order" fails becauseTMPDIRsits under the/varsymlink (758d773f57, #235). It passes with a real-path temp directory:TMPDIR=/private/tmp/<dir>. - In a fresh worktree, the first web runs can fail in browser test files that load no tests
("There was an error when mocking a module") or time out, because Vite re-optimizes
dependencies mid-run and reloads ("optimized dependencies changed. reloading"). Rerun
pnpm test:web:coderuntil no reload is logged; failures that remain are real. - Fork-adapted unit tests that match rendered HTML as strings (
ChatMarkdown.test.tsx) fail when upstream adds an attribute its browser tests never assert (ed3f965e07: #22777'sdir). Add the attribute to the expected string.
-
Commit the merge alone:
git commitwith the subjectMerge upstream/main <short sha> into coder-onlyand a body naming what upstream brought and how conflicts were resolved. -
Commit each Coder adaptation separately after it.
-
git push -u origin <branch>. -
gh pr create --base coder-only --head <branch>. Always pass--base coder-only; never rely on the repository default and never targetmain. -
Register the PR with T3's
link_pull_requesttool when it is available. -
gh pr view <number> --json baseRefName,headRefOid:baseRefNamemust becoder-onlyandheadRefOidmust equalgit rev-parse HEAD. -
gh pr merge <number> --merge --admin --match-head-commit <sha>: thecoder-onlyruleset requires a review the owner cannot give themselves. Keep the branch (no--delete-branch). -
git fetch origin && git log -1 --format=%P origin/coder-only: the second parent is<sha>. -
Update the seams doc's "checked against" SHAs for carried fixes still present upstream, and add to this file wherever it fell short during the sync.
-
Close the issues the sync resolved, with a comment naming the upstream commit, and open issues for bugs it surfaced but left unfixed.