diff --git a/docs/providers/claude.md b/docs/providers/claude.md index bbf72722cf1f..b6810d8c8965 100644 --- a/docs/providers/claude.md +++ b/docs/providers/claude.md @@ -24,20 +24,26 @@ In T3 Code Settings, your Claude provider can stay like this: ```text Display name: Claude Binary path: claude -Claude HOME path: empty +CLAUDE_CONFIG_DIR path: empty ``` -An empty `Claude HOME path` means T3 Code uses your normal home directory. +An empty `CLAUDE_CONFIG_DIR path` means T3 Code sets no `CLAUDE_CONFIG_DIR` at all. The Claude Code +process inherits the server's environment and uses its own default config directory, so it sees your +normal login. -## I Want Work And Personal Claude Accounts +> **Do not type `~/.claude` into that field.** Leaving it empty and pointing it at the default +> directory are not the same thing. Setting `CLAUDE_CONFIG_DIR` explicitly changes how Claude Code +> looks up its stored credentials, and the account stops being identified — `claude auth status` +> still reports `"loggedIn": true` but returns `"email": null`, so T3 Code can no longer show you +> which account the instance is using. Leave the field empty for your main account. -Use a different Claude home for each account. +## I Want Work And Personal Claude Accounts -Example: +Give each extra account its own config directory. T3 Code never changes `HOME`. ```text -default home work account -~/.claude_personal_home personal account +(leave empty) work account Claude Code's own default config dir +~/.claude-personal personal account isolated CLAUDE_CONFIG_DIR ``` ### Set Up The First Account @@ -53,39 +59,74 @@ In T3 Code Settings: ```text Display name: Claude Work Binary path: claude -Claude HOME path: empty +CLAUDE_CONFIG_DIR path: empty ``` ### Set Up The Second Account -Log in with a separate home: +Log in with a separate config directory: ```bash -mkdir -p ~/.claude_personal_home -HOME=~/.claude_personal_home claude auth login +mkdir -p ~/.claude-personal +CLAUDE_CONFIG_DIR=~/.claude-personal claude auth login ``` -Then add another Claude provider in T3 Code: +> **Use `CLAUDE_CONFIG_DIR`, not `HOME`.** Overriding `HOME` also moves the macOS login-keychain +> lookup (`$HOME/Library/Keychains`), so the CLI cannot find its stored OAuth credentials and reports +> "Not logged in". T3 Code stopped setting `HOME` for exactly this reason — see the comment in +> `apps/server/src/provider/Drivers/ClaudeHome.ts`. The path you use here must match the path you put +> in the provider's `CLAUDE_CONFIG_DIR path` field. + +Confirm the second account landed where you expect, before touching T3 Code at all: + +```bash +CLAUDE_CONFIG_DIR=~/.claude-personal claude auth status +``` + +That prints JSON. You want `"loggedIn": true` and the `"email"` of your _second_ account. Running +`claude auth status` with no prefix should still show your _first_ account — if both print the same +email, the second login did not go into the isolated directory. + +Then add another Claude provider in T3 Code — Settings → Providers → the `+` button: ```text Display name: Claude Personal Binary path: claude -Claude HOME path: ~/.claude_personal_home +CLAUDE_CONFIG_DIR path: ~/.claude-personal ``` -Use the email shown in Settings to confirm each provider is using the intended account. Emails are -blurred by default; click the blurred email to reveal it. +Type a Display name first: it fills in the Instance ID for you, and the wizard will not let you past +the Identity step until the Instance ID is valid. + +### Confirm Both Accounts In The App + +Each provider card shows `Authenticated as · ` once its account is detected. Emails are +blurred by default; click a blurred email to reveal it. Comparing the two revealed emails is the only +in-app proof that the two instances really are different accounts. + +## How Do I Switch Between Accounts? + +Pick the account when you start a thread, from the provider rail in the model picker. Each configured +Claude instance appears as its own entry, so choosing "Claude Personal" instead of "Claude Work" +starts that thread on the personal account. Your choice sticks for subsequent new threads. ## Can I Switch Claude Accounts In An Existing Thread? -Usually, no. +No — accounts are chosen per thread, at the start. -T3 Code only offers Claude providers that use the same Claude home for an existing thread. A -different Claude home is treated as a different Claude environment. +Once a thread has started on one Claude account, the other Claude instances are shown greyed out in +the model picker, with the tooltip " is unavailable in this thread. Start a new thread to +switch providers." Their models are removed from the model list too. Start a new thread to use the +other account. -This is different from the recommended Codex setup. Claude Code keeps account and local state across -multiple files under its home directory, so T3 Code keeps separate Claude homes isolated instead of -trying to share part of the state. +Claude Code keys its login and local state to its config directory — and on macOS to a keychain entry +tied to that directory — so T3 Code treats two different `CLAUDE_CONFIG_DIR path` values as two +different environments rather than trying to share part of the state. This is different from the +recommended Codex setup. + +One sharp edge worth knowing: an empty `CLAUDE_CONFIG_DIR path` and an explicit `~/.claude` are +treated as two _different_ environments even though they point at the same account, which is the +other reason not to type the default path into that field. ## I Want To Use OpenRouter @@ -102,7 +143,7 @@ Add or edit a Claude provider in T3 Code Settings: ```text Display name: Claude OpenRouter Binary path: claude -Claude HOME path: ~/.claude_openrouter_home +CLAUDE_CONFIG_DIR path: ~/.claude-openrouter ``` In that provider's Environment variables section, add: @@ -116,15 +157,19 @@ ANTHROPIC_API_KEY Empty value Mark `ANTHROPIC_AUTH_TOKEN` as sensitive. T3 Code stores the value as a server secret and does not send it back to the app after saving. -If you want this setup isolated from your normal Claude account, create that home first: +If you want this setup isolated from your normal Claude account, create that config directory first: ```bash -mkdir -p ~/.claude_openrouter_home +mkdir -p ~/.claude-openrouter ``` -If you previously used the same Claude home with a normal Anthropic login, run `/logout` in a Claude -Code session for that home before using OpenRouter. Otherwise Claude Code may keep using cached -Anthropic credentials instead of the OpenRouter token. +If you previously used the same config directory with a normal Anthropic login, log out of it before +using OpenRouter — otherwise Claude Code may keep using cached Anthropic credentials instead of the +OpenRouter token: + +```bash +CLAUDE_CONFIG_DIR=~/.claude-openrouter claude auth logout +``` ### Pick OpenRouter Models @@ -189,7 +234,7 @@ Configure a Claude provider: ```text Display name: Claude Router Binary path: claude -Claude HOME path: ~/.claude_router_home +CLAUDE_CONFIG_DIR path: ~/.claude-router ``` Then copy the variables that `ccr activate` would export into the provider's Environment variables @@ -199,10 +244,10 @@ If you want the router-backed setup to stay separate from your normal Claude acc in with a dedicated home first: ```bash -mkdir -p ~/.claude_router_home +mkdir -p ~/.claude-router ccr start ccr activate -HOME=~/.claude_router_home claude auth login +CLAUDE_CONFIG_DIR=~/.claude-router claude auth login ``` Claude Code Router's setup can change over time. Use its upstream README for the current install and @@ -218,7 +263,7 @@ Examples: - "Claude Router" - "Claude Experimental" -If the preset needs different Claude files, give it a different `Claude HOME path`. If it needs +If the preset needs different Claude files, give it a different `CLAUDE_CONFIG_DIR path`. If it needs different API keys, base URLs, or router settings, use Environment variables. Do not put environment variable assignments in `Launch arguments`. diff --git a/docs/t3x/SEAMS.md b/docs/t3x/SEAMS.md index 4c28f8a4e0f2..830764e4dadd 100644 --- a/docs/t3x/SEAMS.md +++ b/docs/t3x/SEAMS.md @@ -1,70 +1,125 @@ # t3x seam ledger -**The authoritative list of every upstream-owned line this fork edits.** - -The fork's entire conflict surface against `pingdotgg/t3code` is what appears below. -Everything else the fork adds lives in new, upstream-invisible files (`apps/**/t3x/…`, -`scripts/t3x/…`, `docs/t3x/…`, `.github/workflows/t3x-*.yml`) and can never conflict. - -> **Rule:** a new feature registers itself inside `apps/server/src/t3x/index.ts` -> (or the equivalent per-surface aggregator) — **never** by adding a new edit to an -> upstream file. If a change genuinely cannot avoid touching upstream code, it gets a -> row here and a line in the "why unavoidable" column. If this table grows past a -> handful of rows, re-isolate rather than accept more daily sync pain. - -## Server (`apps/server`) - -| Upstream file | Edit | Why unavoidable | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps/server/src/server.ts` | +1 import of `T3xLayerLive` **and `T3xRoutesLive`** (one statement), +1 `Layer.provideMerge(T3xLayerLive)`, +1 `T3xRoutesLive` entry in `makeRoutesLayer`'s route list | The root layer graph and the route list are both composed here, so a feature layer and a feature route each need exactly one mount point. Both fan in through `t3x/index.ts` — the import is one statement and the route list one entry — so this seam stays 3 lines no matter how many features or routes are added. | - -> The auto-resume reactor **self-starts** via `Effect.forkScoped` at layer construction, -> so — unlike the built-in reactors — it needs **no** `.start()` call in -> `serverRuntimeStartup.ts` or `OrchestrationReactor.ts`. Those files stay untouched. +**The authoritative list of every upstream-owned file this fork edits.** + +Measured, not asserted: **34 upstream-owned files, +1466 / -112 lines**, against merge-base +`89c5a192f`. Everything else the fork adds lives in new files upstream has never seen and cannot +conflict. + +Regenerate this ledger before trusting it — see [Regenerating](#regenerating) at the bottom. An +earlier version of this file claimed the surface was 2 files and "Contracts / persistence: _None._" +while it was in fact 34 files including a persisted schema change, which is how issue #29 (a +recurring rebase conflict in a file this doc said the fork did not touch) went unnoticed. + +> **Rule:** a new feature registers itself through `apps/server/src/t3x/index.ts` (or the equivalent +> per-surface aggregator) — **never** by adding a fresh edit to an upstream file. If a change +> genuinely cannot avoid touching upstream code, it gets a row here. +> +> **Tripwire:** the surface is already far past "a handful of rows". Before adding row 35, re-isolate +> something instead. Prefer fork-owned files even when an in-place edit is smaller. + +## Reading the risk column + +`risk = (fork lines changed) × (upstream commits touching that file in the 60 days before the +merge-base)`. It is a rebase-pain estimate, not a correctness signal: a big fork edit to a file +upstream never touches is cheap, and a two-line edit to a file upstream rewrites weekly is expensive. + +## The ledger + +Sorted by risk, worst first. + +| Upstream file | fork Δ | churn | risk | Why the fork touches it | +| ----------------------------------------------------------------------- | -------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `apps/web/src/components/ChatView.tsx` | +151/-1 | 55 | **8360** | Thread outbox: `handleQueueComposerSubmission`, queue-mode state, `onSend` early-return, ``, `sendLabel` | +| `pnpm-lock.yaml` | +83 | 56 | **4648** | Web Push adds `web-push` + `@types/web-push`. Unavoidable and always conflicts; regenerate rather than merge | +| `packages/client-runtime/src/connection/supervisor.test.ts` | +363 | 3 | **1089** | Issue #21: 356-line appended `describe` + harness plumbing | +| `apps/web/src/components/settings/SettingsPanels.tsx` | +58 | 13 | **754** | Needs-input notifications: import, 3 restore-reducer entries, permission state, a 45-line `` | +| `apps/server/src/serverRuntimeStartup.test.ts` | +149/-1 | 5 | **750** | Crash-recovery reconciler coverage | +| `packages/client-runtime/src/connection/supervisor.ts` | +164/-47 | 3 | **633** | Issue #21: in-place rewrite of the reconnect/backoff state machine, incl. a 41-line upstream block replaced by 2 | +| `packages/contracts/src/ipc.ts` | +28 | 22 | **616** | `DesktopNotificationRequest` / `Activation` + two optional `DesktopBridge` members | +| `apps/web/src/components/chat/ChatComposer.tsx` | +16/-2 | 30 | **540** | Threads `sendLabel` / `canQueue` through the composer | +| `apps/mobile/src/features/threads/ThreadComposer.tsx` | +26/-4 | 16 | **480** | Mobile Return-key send/queue | +| `apps/server/src/serverRuntimeStartup.ts` | +29 | 6 | **174** | `reconcile.interrupted-turns` startup phase | +| `apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift` | +33 | 5 | **165** | Shift+Return newline vs. bare Return submit | +| `apps/desktop/src/preload.ts` | +12 | 13 | **156** | `showNotification` + `onNotificationActivated` on the exposed bridge | +| `apps/desktop/src/backend/DesktopBackendConfiguration.ts` | +29 | 5 | **145** | Backend heap headroom (`NODE_OPTIONS`) | +| `apps/web/src/components/chat/ComposerPrimaryActions.tsx` | +53/-13 | 2 | **132** | Queue button; extracts upstream's inline stop button | +| `apps/desktop/src/backend/DesktopBackendConfiguration.test.ts` | +42 | 3 | **126** | Heap-headroom assertions | +| `packages/shared/src/composerTrigger.test.ts` | +31/-1 | 3 | **96** | `replaceTextRange` newline coverage | +| `apps/server/src/sourceControl/SourceControlProviderDiscovery.ts` | +20/-8 | 3 | **84** | Issue #4: CLI probe timeout + spawn-error classification | +| `apps/server/src/server.ts` | +3 | 25 | **75** | The intended mount point: one import, one `Layer.provideMerge`, one route entry | +| `packages/contracts/src/settings.ts` | +7/-2 | 14 | **70** | `notifyOnNeedsInput` (**persisted schema**) + Claude `homePath` placeholder/description | +| `apps/desktop/src/main.ts` | +4 | 17 | **68** | `ElectronNotification` layer | +| `apps/web/src/connection/platform.ts` | +7/-1 | 7 | **56** | Lazy `import()` of outbox cleanup to dodge a module-init cycle | +| `apps/web/src/routes/__root.tsx` | +6 | 9 | **54** | Mounts ``, ``, `` | +| `apps/mobile/…/T3ComposerEditorView.kt` | +51 | 1 | **51** | Android bare-Enter intercept | +| `apps/desktop/src/ipc/channels.ts` | +2 | 11 | **22** | Two notification channel constants | +| `apps/server/package.json` | +2 | 11 | **22** | `web-push` dependency | +| `apps/desktop/src/ipc/DesktopIpcHandlers.ts` | +2 | 9 | **18** | Registers the `showNotification` handler | +| `apps/mobile/src/native/T3ComposerEditor.types.ts` | +5/-1 | 3 | **18** | Reworded `onSubmit` doc comment | +| `apps/web/src/routes/_chat.$environmentId.$threadId.tsx` | +2 | 4 | **8** | Mounts `` | +| `apps/web/index.html` | +5 | 1 | **5** | PWA manifest + meta tags | +| `apps/desktop/src/settings/DesktopClientSettings.test.ts` | +1 | 5 | **5** | `notifyOnNeedsInput` in a fixture | +| `apps/mobile/src/native/T3ComposerEditor.native.tsx` | +3 | 1 | **3** | Plumbs `onComposerSubmit` | +| `apps/mobile/src/components/AppSymbol.tsx` | +2 | 1 | **2** | `return:` icon entry | +| `apps/mobile/…/T3ComposerEditorModule.kt` | +1 | 1 | **1** | Event-name list entry | +| `docs/providers/claude.md` | +76/-31 | ~0 | **~0** | Fixes the broken multi-account recipe. Near-frozen upstream (last touched 2026-04-29) — **the one row worth upstreaming**, which would remove it | + +**Per surface:** `apps/web` 8 · `apps/mobile` 7 · `apps/desktop` 7 · `apps/server` 5 · +`packages/**` 5 · `docs/` 1 · repo root 1. + +### Files deliberately removed from this surface + +- `packages/contracts/src/settings.test.ts` — the fork's `notifyOnNeedsInput` block sat at the same + `describe` anchor upstream keeps appending to, producing the add/add conflict in issue #29. Moved + to `packages/contracts/src/t3x/settings.t3x.test.ts`; the upstream file is byte-identical again. + **This is the pattern to copy:** fork test cases belong in a `t3x/` sibling, never appended to an + upstream spec. ## Logic mirrors (semantic dependencies, not code seams) -These are upstream helpers whose logic the fork **replicates** (rather than imports, to -avoid a code seam). They don't conflict during rebase, but if upstream changes the -original's behavior the mirror can drift silently — the daily sync agent must diff these -originals when they change. +Upstream helpers the fork **replicates** rather than imports, to avoid a code seam. These never +conflict during rebase, so nothing warns you when the original changes and the mirror drifts. -| Fork mirror | Mirrors upstream | Risk if upstream changes | -| ------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps/server/src/t3x/autoResume/guards.ts` (`hasOpenBlockingRequest`) | `decider.ts:57-80` (private, unexported) | Awaiting-approval guard could miss a new blocking-request activity kind and auto-resume into a prompt. | -| `apps/server/src/t3x/autoResume/http.ts` (`authenticateWithOperateScope`) | `http.ts:78-95` (`authenticateRawRouteWithScope`, private, unexported) | If upstream changes how raw routes authenticate (new error case, different scope check), `/api/t3x/auto-resume` could authenticate more weakly than the routes beside it. | +| Fork mirror | Mirrors upstream | Risk if upstream changes | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `apps/server/src/t3x/autoResume/guards.ts` (`hasOpenBlockingRequest`) | `decider.ts` (private, unexported) | Could miss a new blocking-request activity kind and auto-resume into a prompt. | +| `apps/server/src/t3x/autoResume/http.ts` (`authenticateWithOperateScope`) | `http.ts` (`authenticateRawRouteWithScope`, private, unexported) | `/api/t3x/auto-resume` could authenticate more weakly than the routes beside it. | -## Web (`apps/web`) +## Files owned entirely by the fork (not seams) -| Upstream file | Edit | Why unavoidable | -| -------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `apps/web/src/routes/_chat.$environmentId.$threadId.tsx` | +1 import, +1 `` sibling of `` | A per-thread overlay has to be rendered by the thread route; there is no extension point. Chosen as a floating card precisely so it needs no cooperation from upstream layout — no `ChatView.tsx` / `ChatComposer` edits (both hot files). | +- `apps/server/src/t3x/**`, `apps/web/src/t3x/**`, `packages/contracts/src/t3x/**` — feature code + and the `T3xLayerLive` aggregator. +- `scripts/t3x/**` — fork setup, upstream sync, desktop auto-build (incl. opt-in git hooks). +- `.github/workflows/t3x-*.yml` — `t3x-upstream-sync.yml`, `t3x-weekly-verify.yml`, + `t3x-sync-resolve.yml`, `t3x-ci.yml` (the fork's PR/main gate; upstream's `ci.yml` needs + blacksmith runners the fork cannot use). +- `docs/t3x/**`, `docs/superpowers/specs/**`. -> The overlay ships to the **desktop app too**: `apps/desktop` is an Electron shell that -> loads this same web bundle from `t3code://app`, so there is no separate desktop UI to -> change. `apps/mobile` is a distinct React Native codebase and does **not** get it. +Note that a fork-created file is only conflict-free if upstream never creates a file at the same +path. Roughly half of the fork's new files sit outside the four `t3x`-named namespaces above, so +that guarantee is weaker than it looks. -## Contracts / persistence +### Desktop auto-build (`scripts/t3x/auto-build-desktop.sh`) -_None._ (No schema or migration changes — t3x state is a plain atomic-written JSON -file, deliberately avoiding the upstream-owned migration registry.) +**Zero seams.** Shells out to the existing `pnpm dist:desktop:dmg:arm64` rather than importing or +editing `scripts/build-desktop-artifact.ts` (hot), and deliberately adds **no** script entry to the +root `package.json` (also hot) — it is invoked by path. See `docs/t3x/auto-build-runbook.md`. ---- +## Regenerating -## New files owned entirely by the fork (not seams — listed for orientation) +```bash +MB=$(git merge-base main upstream/main) +git diff --numstat "$MB"..HEAD | while read -r a d p; do + git cat-file -e "$MB:$p" 2>/dev/null && printf '%s\t%s\t%s\n' "$a" "$d" "$p" +done +``` -- `apps/server/src/t3x/**` — feature code + the `T3xLayerLive` aggregator. -- `scripts/t3x/**` — fork setup, upstream-sync, and desktop auto-build scripts - (incl. `scripts/t3x/hooks/**`, opt-in git hooks that are never auto-installed). -- `.github/workflows/t3x-*.yml` — `t3x-upstream-sync.yml`, `t3x-weekly-verify.yml`, - `t3x-sync-resolve.yml`, `t3x-ci.yml` (the fork's PR/main gate; upstream's `ci.yml` needs - blacksmith runners the fork can't use). -- `docs/t3x/**`, `docs/superpowers/specs/2026-07-23-*`. +That prints exactly the upstream-owned files the fork edits. Churn for any one of them: -### Desktop auto-build (`scripts/t3x/auto-build-desktop.sh`) +```bash +git log --oneline --since="60 days ago" "$MB" -- | wc -l +``` -**Zero seams.** Rebuilds/installs the macOS `.dmg` when `HEAD` moves. It shells -out to the existing `pnpm dist:desktop:dmg:arm64` rather than importing or -editing `scripts/build-desktop-artifact.ts` (a hot upstream file), and -deliberately adds **no** script entry to the root `package.json` (also hot) — -it is invoked by path. See `docs/t3x/auto-build-runbook.md`. +Re-run both after every upstream sync and update this file. If the ledger and this document +disagree, the ledger is right. diff --git a/docs/t3x/sync-agent-runbook.md b/docs/t3x/sync-agent-runbook.md index 2ab1bb05c3fd..c42254d1e2ee 100644 --- a/docs/t3x/sync-agent-runbook.md +++ b/docs/t3x/sync-agent-runbook.md @@ -3,10 +3,12 @@ The daily GitHub Action (`t3x-upstream-sync.yml`) does the mechanical rebase every day for free. When it **can't** complete — a merge conflict, a red verify, or a dropped patch — it opens (or updates) a single `t3x-sync` issue with a status JSON block. This runbook is how that -issue gets resolved: **on demand, by activating an agent**. Nothing runs on clean days. +issue gets resolved: **on demand, by activating an agent**. This resolver runs only when asked — +though `t3x-ci.yml` still gates every push and PR, and `t3x-weekly-verify.yml` runs on Sundays. -See the design specs `2026-07-23-fork-upstream-sync-design.md` (the daily/weekly sync + the -`t3x-sync` escalation contract) and `2026-07-25-sync-conflict-agent-design.md` (this resolver). +See the design specs `docs/superpowers/specs/2026-07-23-fork-upstream-sync-design.md` (the +daily/weekly sync + the `t3x-sync` escalation contract) and +`docs/superpowers/specs/2026-07-25-sync-conflict-agent-design.md` (this resolver). ## Activate the agent (the one action) @@ -21,22 +23,61 @@ the rebase, resolve the conflicts, run verify, and **open a PR into `main`**. It `main` — you review it and land it yourself (see [Landing a sync PR](#landing-a-sync-pr-do-not-use-the-github-merge-button)). The agent comments the PR link back on the issue when done. -Prefer a button? Run it manually instead (optionally pass the issue number and bump the model -for a gnarly merge): +> **The comment path always uses `claude-sonnet-5`.** The `model` input only exists on manual +> dispatch, so `@claude resolve` cannot select a stronger model. For anything but a small range, +> dispatch it instead: +> +> ``` +> gh workflow run "t3x sync resolve (agent)" -R radroid/t3code -f issue= -f model=claude-opus-5 +> ``` + +**Check the budget before you pick a path.** The job is capped at `timeout-minutes: 45` and +`--max-turns 150`. The one successful resolve to date absorbed a 36-commit upstream range against +37 fork patches and used 36m15s — 81% of the budget. A materially larger range will exhaust it, and +a timeout mid-rebase leaves an unpushed branch and a spent budget. Raising the caps needs a workflow +edit; `workflow_dispatch` reads the workflow file from `--ref`, so you can carry raised limits on a +scratch branch without merging it (the job still checks out and rebases `main`): ``` -gh workflow run "t3x sync resolve (agent)" -R radroid/t3code -f issue= -f model=claude-opus-5 +gh workflow run t3x-sync-resolve.yml -R radroid/t3code \ + --ref t3x/bigger-resolve-budget -f issue= -f model=claude-opus-5 ``` +Measure the range first: + +``` +git fetch upstream +git rev-list --count $(git merge-base main upstream/main)..upstream/main # upstream commits +git rev-list --count --no-merges upstream/main..main # fork patches to replay +``` + +### The daily job cannot trigger the resolver + +Its escalation comment contains the literal `@claude resolve` string, but GitHub creates no workflow +run from a `GITHUB_TOKEN`-authored event, and `github-actions[bot]` reports `author_association: +NONE`, which fails the workflow's own permission check. Resolution is always human-initiated. Do not +"fix" the phrasing in the escalation body — it is the copy-pasteable instruction for the human. + +A comment that fails the gate (missing `t3x-sync` label, wrong author, edited rather than newly +created) shows up in the Actions tab as **skipped**, not as an error. Check there if nothing happens. + +### Not every `t3x-sync` issue is a rebase conflict + +`t3x-weekly-verify.yml` escalates onto the _same_ label and issue with `**kind:** weekly-build`. +`@claude resolve` passes the full gate on those too and will spend an entire agent budget replaying +a rebase that is not the problem. Read the `**Result:**` / `**kind:**` line first; a `weekly-build` +failure means fix the build. + ## What the agent does (and what a human doing it locally should do) This is the checklist the workflow prompt mirrors — follow it if you resolve locally instead. 1. `git fetch upstream && git switch -c t3x/sync- main` 2. `git rebase upstream/main`. Resolve each conflict by understanding intent — favour - upstream's structure while preserving the fork's t3x behaviour. `rerere` (enabled locally) - auto-applies anything resolved before. `git add -A && git rebase --continue`. -3. **Do the thing CI cannot:** review the upstream commits that touched t3x *seams* + upstream's structure while preserving the fork's t3x behaviour. `git add -A && git rebase --continue`. + `rerere` auto-applies anything resolved before, but **only in a local clone** (`setup-fork.sh` + enables it) — the CI resolver gets no rerere replay. +3. **Do the thing CI cannot:** review the upstream commits that touched t3x _seams_ (`docs/t3x/SEAMS.md`) even when they did **not** textually conflict — upstream may have changed the semantics of an API the fork hooks into. For each seam file, `git log ..upstream/main -- ` and read the diffs; confirm the fork's feature @@ -56,29 +97,75 @@ This is the checklist the workflow prompt mirrors — follow it if you resolve l ## Landing a sync PR (do NOT use the GitHub merge button) The resolver's branch is the fork's patch series **rebased onto new upstream**, so `main` is -*not* an ancestor of it. GitHub reports the PR `CONFLICTING`/`DIRTY` (a huge diff plus add/add +_not_ an ancestor of it. GitHub reports the PR `CONFLICTING`/`DIRTY` (a huge diff plus add/add conflicts on the fork's own files), and **Merge / Squash / Rebase all fail** — the branch is -meant to *replace* `main`'s history, not extend it. Land it by force-updating `main` to the -reviewed tip instead: +meant to _replace_ `main`'s history, not extend it. Land it by force-updating `main` to the +reviewed tip instead. + +The branch is `t3x/sync-` — the run id, not the issue number. A **draft** PR means +the resolver could not get all three verify steps green; read its issue comment before anything else. + +**1. Get a CI signal — it will not appear on its own.** `t3x-ci.yml` does have a `push` trigger on +`t3x/sync-**`, but the resolver pushes with `GITHUB_TOKEN`, and GitHub creates no workflow runs from +`GITHUB_TOKEN`-authored events. Neither that trigger nor `pull_request` fires. Dispatch it: + +``` +gh workflow run t3x-ci.yml -R radroid/t3code --ref t3x/sync- +gh run list -R radroid/t3code --workflow t3x-ci.yml --branch t3x/sync- +``` + +**2. Review what the resolver changed in each fork patch.** A plain `git diff` against `main` is a +useless whole-upstream delta; use `range-diff`: + +``` +git fetch origin && git fetch upstream +OLD=$(git merge-base origin/main upstream/main) +NEW=$(git merge-base origin/t3x/sync- upstream/main) +git range-diff "$OLD..origin/main" "$NEW..origin/t3x/sync-" +``` + +Confirm no fork patch silently vanished — `git rev-list --count --no-merges upstream/main..origin/t3x/sync-` +should equal the pre-sync patch count. + +**3. Land it.** Save the old `main` first; step 5 needs it. ``` git fetch origin -# review the branch first (checklist above), then point main at its exact tip: +OLD_MAIN=$(git rev-parse origin/main) # SAVE THIS git push --force-with-lease origin origin/t3x/sync-:main -gh pr close --comment "Landed by force-updating main to " ``` -- `main` has **no branch protection**, so the force-push is allowed but unguarded — the only - rollback is the `t3x/last-good-*` tag the daily Action pushes before each rebase. -- The PR will **not** auto-mark as merged after a force-update; close it manually (as above). -- **No independent CI gates the PR** — `ci.yml` runs on `blacksmith-*` runners the fork can't - use, so the only verify is the resolve job's own `vp run typecheck/lint/test`. To re-gate - locally before landing: +`--force-with-lease` leases against your local `refs/remotes/origin/main`, so the `git fetch` +immediately before is required. It works from any worktree with no branch checked out. + +**4. Close out.** GitHub usually auto-marks the PR `MERGED` on force-update, in which case +`gh pr close` errors — check first. The **issue** is what actually needs closing by hand. + +``` +gh pr view -R radroid/t3code --json state +gh issue close -R radroid/t3code --comment "Landed by force-updating main" +``` + +**5. Rebase in-flight branches with `--onto`, not a plain rebase.** `main`'s history was _replaced_, +so `git rebase origin/main` would try to replay all of pre-sync `main`: + +``` +git rebase --onto origin/main "$OLD_MAIN" t3x/ +git push --force-with-lease origin t3x/ +``` + +Any branch left un-rebased shows `[origin/main: ahead N, behind M]` and cannot land. + +- `main` has **no branch protection** (verified: no protection, no rulesets), so the force-push is + allowed but unguarded. Rollback is the `t3x/last-good-*` tag from the escalation issue: + `git push --force-with-lease origin t3x/last-good-^{commit}:main`. The tag is cut per daily + run on pre-rebase `main`, so if feature PRs merged after it, rolling back drops them. If the tag + push failed the issue says `none` and there is no rollback point. +- Repair local checkouts: `git fetch origin && git branch -f main origin/main` (a plain + `git checkout main` fails if another worktree holds it). The auto-build worktree needs no action — + it force-detaches to the freshly fetched sha on every tick. +- To re-gate locally before landing: `git switch --detach origin/t3x/sync- && vp run typecheck && vp run lint && vp run test`. -- **After landing, every other in-flight branch is behind the sync** and must be rebased onto the - new `main` before its PR can land (they were built on pre-sync history; expect the same class - of conflicts on shared t3x files). Reset any stale local `main` too: - `git checkout main && git fetch origin && git reset --hard origin/main`. ## One-time setup diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 64dd3e35a4f1..e8eaf723e174 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -49,21 +49,6 @@ describe("ClientSettings glass opacity", () => { }); }); -describe("ClientSettings needs-input notifications", () => { - it("defaults needs-input notifications on for existing installs", () => { - expect(decodeClientSettings({}).notifyOnNeedsInput).toBe(true); - }); - - it("honours an explicit opt-out", () => { - expect(decodeClientSettings({ notifyOnNeedsInput: false }).notifyOnNeedsInput).toBe(false); - }); - - it("accepts the toggle in a client settings patch", () => { - expect(decodeClientSettingsPatch({}).notifyOnNeedsInput).toBeUndefined(); - expect(decodeClientSettingsPatch({ notifyOnNeedsInput: false }).notifyOnNeedsInput).toBe(false); - }); -}); - describe("ClientSettings sidebar v2", () => { it("defaults the beta off with a three-day auto-settle threshold", () => { const settings = decodeClientSettings({}); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7f981131f297..106253bb1547 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -258,8 +258,8 @@ export const ClaudeSettings = makeProviderSettingsSchema( Schema.annotateKey({ title: "CLAUDE_CONFIG_DIR path", description: - "Custom Claude home and config directory. Keeps .claude.json and .claude separate.", - providerSettingsForm: { placeholder: "~/.claude", clearWhenEmpty: "omit" }, + "Isolated config directory for this instance, so a second Claude account stays separate. Leave empty to use Claude Code's own default. Do not enter ~/.claude — pointing CLAUDE_CONFIG_DIR at the default directory is not the same as leaving this empty and stops the account being identified.", + providerSettingsForm: { placeholder: "e.g. ~/.claude-personal", clearWhenEmpty: "omit" }, }), ), customModels: Schema.Array(Schema.String).pipe( diff --git a/packages/contracts/src/t3x/settings.t3x.test.ts b/packages/contracts/src/t3x/settings.t3x.test.ts new file mode 100644 index 000000000000..b4d790feaae7 --- /dev/null +++ b/packages/contracts/src/t3x/settings.t3x.test.ts @@ -0,0 +1,34 @@ +// Fork-owned coverage for t3x additions to the shared settings contracts. +// +// These cases deliberately live OUTSIDE `packages/contracts/src/settings.test.ts`. +// That file is upstream-owned and churns hard (8 commits in 30 days), and the fork's +// block sat at the same `describe` anchor upstream keeps inserting at — which produced +// a recurring add/add rebase conflict on the daily upstream sync (issue #29). +// Keeping fork tests in a fork-owned file removes that file from the conflict surface. +// See docs/t3x/SEAMS.md. +// +// The two decode helpers below are re-declared rather than imported because the +// upstream test file keeps them module-local. + +import { describe, expect, it } from "vite-plus/test"; +import * as Schema from "effect/Schema"; + +import { ClientSettingsSchema, ClientSettingsPatch } from "../settings.ts"; + +const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema); +const decodeClientSettingsPatch = Schema.decodeUnknownSync(ClientSettingsPatch); + +describe("ClientSettings needs-input notifications", () => { + it("defaults needs-input notifications on for existing installs", () => { + expect(decodeClientSettings({}).notifyOnNeedsInput).toBe(true); + }); + + it("honours an explicit opt-out", () => { + expect(decodeClientSettings({ notifyOnNeedsInput: false }).notifyOnNeedsInput).toBe(false); + }); + + it("accepts the toggle in a client settings patch", () => { + expect(decodeClientSettingsPatch({}).notifyOnNeedsInput).toBeUndefined(); + expect(decodeClientSettingsPatch({ notifyOnNeedsInput: false }).notifyOnNeedsInput).toBe(false); + }); +});