fix(sync): resolve 8 Nous upstream conflicts blocking the nightly (513 commits) - #16
Closed
vashkartik wants to merge 2909 commits into
Closed
fix(sync): resolve 8 Nous upstream conflicts blocking the nightly (513 commits)#16vashkartik wants to merge 2909 commits into
vashkartik wants to merge 2909 commits into
Conversation
…eletion Addresses @egilewski's review: the parent-directory check still deleted checkpoint history for the most common unmount layout. Detaching storage removes the parent outright in some layouts (`/Volumes/Ext/proj` on macOS, `/media/<user>/<label>/proj`), which the first commit handles. But in the classic static layout — `/mnt/volume/proj`, an fstab entry, a container bind-mount — unmounting removes the contents and leaves the mount point behind as an empty directory. `parent.is_dir()` is then true, the project is absent, and the startup sweep deletes its ref, index and metadata: exactly the case this PR set out to protect. Reproduced against the real predicate before this commit: mount root vanished (macOS) -> False ok empty surviving mount point -> True <-- history deleted really deleted (siblings) -> True ok An empty parent carries no information: it looks identical whether the volume was detached or the project was deleted. So require the parent to actually say something — it holds some other entry (we observed a populated directory that does not contain the project), or it is itself a live mount point (the volume is attached right now and demonstrably does not hold the project). The cost is that a project deleted out of an otherwise-empty parent is no longer reclaimed by the orphan rule. It is not leaked: the retention rule reads `last_touch` rather than probing the filesystem and still collects it, so reclamation is deferred, not lost. That is the right direction for a predicate whose false positive destroys a user's restore points unattended. `_dir_has_any_entry` stops at the first entry via `os.scandir` instead of materializing a listing, since a project root can hold a large tree. tests/tools/test_checkpoint_manager.py: `test_surviving_empty_mountpoint_ keeps_its_checkpoints` pins the reviewed case, and `test_empty_parent_project_ is_still_reclaimed_by_retention` pins the deferral above so the safety valve cannot silently regress into a leak. Both fail on the previous commit. The real-orphan control now seeds a sibling so it exercises a populated parent rather than the ambiguous empty one. 80 passed in the checkpoint suite; the 2 remaining failures (`TestGitEnvIsolation`, `TestClearFunctions`) fail identically on clean main.
…orphan classification Follow-up to the cherry-picked NousResearch#69063: egilewski's review found that the _dir_has_any_entry(parent) guard treats ANY entry in the mount point's parent as proof the volume is attached — but unmounting exposes the UNDERLAY directory's own files (e.g. a .keep placeholder), so a populated underlying mount-point dir still classified the project as an orphan and deleted its ref/index/metadata. Reproduced on both main and the PR head. Attachment evidence is now positive instead of circumstantial: * _volume_evidence() records the parent directory's (st_dev, st_ino) identity in the project's metadata while the workdir is observably live (at _register_project/_touch_project time). A mount point resolves to the mounted filesystem's root while attached and to the underlay directory after detach — same path, different directory, different identity. * _workdir_is_observably_gone() now requires the parent visible at prune time to match that recorded identity before the populated-parent check can classify an orphan. A mismatch means a different directory (the underlay) is showing through — a detached volume, not an observed deletion. * Metadata without a recorded identity (written by older versions) is never orphan-classified — unsure never deletes; the retention/stale rule still reclaims genuinely abandoned projects off last_touch. * The frozen pre-v2 layout has no metadata channel for the identity, so it keeps the structural checks only (require_parent_identity=False). * A failed evidence probe on re-registration preserves the previously recorded identity — stale evidence can only make pruning MORE conservative. Windows: st_dev/st_ino of 0 (filesystems without file IDs, some network shares) is treated as "no evidence recorded", which falls into the conservative never-orphan path. os.path.ismount and Path.stat are cross-platform; no POSIX-only calls added. tests/tools/test_checkpoint_manager.py: adds egilewski's exact regression (checkpoint history for mnt/volume/project, detach exposes mnt/volume/.keep, prune with orphan deletion enabled → NOT deleted; fails on the bare cherry-pick, passes with this fix), plus no-recorded-identity conservatism and probe-failure identity preservation. His absent-parent/empty-parent/retention/genuine-deletion/ live-project controls all still pass. Reported-by: egilewski (review on NousResearch#69063)
Use wall deadlines for deleteWebhook and start_polling, then fail cold startup unless getUpdates proves progress. This lets the gateway discard partial PTB state and retry with a fresh adapter.\n\nRefs NousResearch#67498
Give Telegram a 180s default outer connect budget so cold polling can prove getUpdates readiness. Preserve the 30s default for other platforms and all explicit config/env overrides.\n\nRefs NousResearch#67498
…neration Follow-up hardening for the salvaged NousResearch#69240 readiness gate (NousResearch#67498): - _start_polling_once now returns its (generation, progress_event) pair so the strict cold-start gate binds to exactly the generation it started, instead of re-reading self._polling_progress_event which a concurrent recovery task may have replaced with a newer generation's event (the G1/G2 race flagged in the NousResearch#69240 review). - Strict cold start no longer schedules background polling recovery: a polling error during the readiness wait is captured by a strict callback and fails the connect attempt immediately with a loud OSError, so GatewayRunner disposes the partial adapter and retries with a fresh one — no more waiting out the full readiness deadline on a generation that already errored, and no G2-on-partial-app healing. - After readiness is proven the strict callback delegates every later polling error to the real background-recovery callback, preserving the existing degraded/reconnect semantics for the polling lifetime. - The readiness-timeout error message now states the deadline and that the gateway will retry with a fresh adapter (loud failure, not a silent wait). - Regression tests: current-generation progress connects; a polling error during strict cold start fails fast without scheduling background recovery (the NousResearch#67498 idle-threads shape); stale-generation progress is rejected. Progresses NousResearch#67498
The strict cold-start readiness gate (NousResearch#67498) means adapter.connect() no longer returns True until the mocked start_polling records a successful getUpdates round trip for its generation. Update the conflict-suite Application mocks accordingly: - fake_start_polling side effects call adapter._record_polling_progress(adapter._polling_generation) on the initial connect (retry generations intentionally do NOT auto-progress where a test asserts the conflict count survives an unproven retry). - _build_polling_app takes the adapter so its start_polling mock can record progress. Without this, the cold connects in these tests wait out the full 60s readiness deadline and fail — which is exactly the fail-closed behavior the gate is supposed to provide when polling shows no progress.
Both the Desktop panel and the CLI setup flow need somewhere in .env to put a custom endpoint's API key. Deriving the name from the endpoint's hostname collapses two servers on one machine onto a single slot, and every IP-based local endpoint slugs to a digit-leading name that save_env_value rejects outright. Key off the endpoint's own identity and keep a fixed prefix. Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com> Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
…as an update (NousResearch#69179) The desktop self-update chain (Desktop -> hermes-setup --update -> hermes update -> hermes desktop --build-only -> relaunch) rebuilds Hermes.exe on the user's machine and declared success on bare file EXISTENCE. A truncated PE (corrupt cached Electron zip / interrupted extraction or rcedit rewrite / full disk) or a wrong-architecture unpacked tree therefore shipped as the 'updated' app, which Windows refuses to load with 'This app can't run on your computer' (此应用无法在你的电脑上运行) — and the previous working build had already been wiped by before-pack.mjs, leaving nothing to fall back to. Fix, in three parts: - hermes_cli/main.py: post-build integrity gate on Windows (_ensure_desktop_exe_launchable). Parses the PE header of the freshly built Hermes.exe — MZ/PE magic, section-table completeness vs file size (catches truncation), and COFF machine vs the host arch (catches arm64/x64 mixups). On failure it purges the (likely corrupt) cached Electron zip, invalidates the content-hash build stamp so the updater's retry-once genuinely re-downloads and rebuilds, restores the previous build from the .bak tree when one exists (keeping the corrupt tree as .corrupt for diagnostics), tells the user the update was aborted and their old version kept, and exits nonzero. _desktop_packaged_executable also now prefers a host-loadable PE over pure newest-mtime when multiple win-*-unpacked trees coexist. - apps/desktop/scripts/before-pack.mjs: on win32, the previous unpacked tree is preserved as <appOutDir>.bak (only when it holds the product exe — partial/corrupt trees still get the plain wipe) instead of being destroyed, providing the rollback material for the gate above. Non-Windows behavior is unchanged. - Behavior-contract tests: tests/hermes_cli/test_desktop_exe_integrity.py (23 tests — synthetic PE fixtures for truncation/non-PE/arch-mismatch, rollback semantics, and the build-only exit contract) and 6 new vitest cases in before-pack.test.mjs for the .bak preservation rules. Progresses NousResearch#69179
…endpoint Test enumerates a custom provider's catalogue and the panel holds the result in discoveredModels, but the save payload never carried it, so only the one model the user hand-typed reached providers.<id>.models. Every downstream picker reads that map straight from config.yaml with no live probe, which is why a proxy serving 18 models offered exactly one. Send the discovered list and merge it onto the entry, so models already known keep their context lengths. Fixes NousResearch#69988 Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com>
…yaml
The Custom Endpoints panel wrote the raw key to providers.<id>.api_key, so
the credential sat in plaintext in a file users routinely share and commit.
The input is masked, so nothing warned them.
Write the key to .env and reference it via key_env, the same indirection
built-in providers use and that runtime_provider already resolves. The read
side has to move with it: reporting has_api_key from api_key alone would
show "no API key" for every migrated endpoint, and activate copying only
api_key would drop the credential entirely. Delete now clears the .env slot
too, and an entry still carrying a pre-fix plaintext key is migrated on its
next save so existing users get cleaned up without re-entering anything —
unless the key is a hand-written ${VAR} template, which is already safe and
must not be duplicated into a second env var.
Fixes NousResearch#69449
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com>
hermes model's custom-endpoint flow is the other write path that produced a plaintext key, on both the model block and the custom_providers entry. Route it through the same .env indirection as the Desktop panel, and swap an existing entry's inline key for the reference when the URL is re-saved. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Bug-class coverage for both fixes: the full catalogue survives Save, context
lengths are preserved, the key never lands in config.yaml on either write
path, blank clears it, a pre-fix plaintext key migrates while a ${VAR}
template is left alone, two endpoints on one host keep separate credentials,
and an IP-derived name is still a valid POSIX env var.
The two delete tests asserted on the plaintext mirror; they now assert the
same invariants against the credential reference.
The desktop gateway passed the vision-enriched, model-only message text (carrying an `image_url:<path>` hint) straight into run_conversation as the persisted user turn. The renderer only parses `@image:<path>`, so it could not rebuild the attachment from history: after a restart the image was gone and only the caption survived, and on a live session switch the warm cache disagreed with the authoritative text and the frontend "rescued" the image by appending it after the caption. run_conversation already supports persist_user_message for exactly this "what the model sees" vs "what gets stored" split; it was simply never wired up for the attachment path.
Persisted history carries no attachment metadata for non-image refs, so resume reconciliation dropped `@file:` chips off a user turn whose text matched. Carry the warm cache's refs forward when the resumed message has none of its own, never replacing refs that are already present. (cherry picked from commit eac5b0a)
The unquoted alternative in the directive pattern is `\S+`, so a ref built by string interpolation truncates at the first space and strands the tail as loose text next to a broken thumbnail. Composer images live in the app's userData dir, which on macOS is `~/Library/Application Support/<App>/` — so every pasted or dropped image hit this. Adds format_reference_value next to REFERENCE_PATTERN, mirroring formatRefValue in the desktop's directive-text.tsx, and covers the round-trip through the parser.
…s too A turn routed to a model that takes pixels directly sends `content` as a parts list, and the session store deliberately ignores a plain-string persist override for a list payload — a text override must not erase a turn's image summary. So the override was dropped for every user on a vision-capable main model, and the durable row kept only the caption plus a literal `[Image attached at: ...]` / `[screenshot]`, which the renderer cannot turn back into an image. Only vision-preprocessed (text-mode) turns were actually fixed. Mirror the shape instead: swap the text part for the `@image:` ref form and keep the image parts, so the model still has the pixels for the rest of the session, and drop the `[screenshot]` stand-in on the way into the bubble when a ref was lifted from the same message.
Matches the two derived values above it and fixes the indentation.
Session previews are the first 60 characters of the first user message, so persisting the @image: directives ahead of the caption labelled the session with a truncated file path in the sidebar, session switcher, and command palette. Clients lift the refs out of the body line by line, so moving them after the caption changes nothing about how the turn renders.
The unit tests cover each layer in isolation, but nothing exercised the whole chain the bug lived in: the real gateway persisting an attachment, SessionDB holding it after the process exits, and the renderer rebuilding a thumbnail from the stored turn. Seeds a session through the real gateway with an image attached, then launches desktop against it — so the first render is already the relaunch case. Pins native image routing (the majority path, and the one where a text-only persist override is dropped) and stages the file behind directory and file names with spaces, mirroring the macOS composer's Application Support path.
Map picker-prefixed custom provider selections back to their configured model IDs before validation, persistence, and API requests. Fixes NousResearch#68347
Attribution check needs a mapping for the cherry-picked commit's author so release notes credit them correctly.
…el-id-resolution fix(model_switch): don't send a picker prefix as the custom provider model id
…point-keys-and-models fix: custom endpoint keys go to .env, and Save keeps the whole model list
…age-persist fix(desktop): keep attached images renderable across session switches and restarts
… commands (NousResearch#71048) A real APPLICATION_COMMAND interaction forwarded over the relay arrived slash-less: _discord_interaction_to_event set text = data['name'] ("new", not "/new"), MessageType.TEXT, and dropped options entirely — so a registered /new dispatched as plain chat instead of a command (MessageEvent.is_command() is text.startswith("/")). Port the connector's Slack slash-command precedent (normalizeSlackCommand builds `${command} ${args}`.trim() with a leading slash and explicit command type): for type-2 interactions build "/" + name, append rendered options space-separated (scalar options contribute their value, matching the native adapter's f"/model {name}" shape; SUB_COMMAND/ SUB_COMMAND_GROUP contribute their name then recurse into nested options), and set MessageType.COMMAND. Type-3 (custom_id) and other interaction types are unchanged. This implements the interaction->command sub-design previously flagged as deferred in the _on_passthrough docstring. Companion connector fix in gateway-gateway: fix(relay): strip own-mention prefix so addressed slash commands dispatch.
Resolve @session:<profile>/<id> reference values to the session's title:
the in-memory sidebar list answers most lookups, and an unknown id falls
back to GET /api/sessions/{id}. Cache, in-flight dedupe, and subscriber
fan-out mirror the external-link title resolver.
An untitled row resolves to empty rather than "Untitled session" so the
caller's short-id fallback stays the chip label.
Route session refs in the transcript through the title resolver so a dropped session reads as its title instead of a truncated id, and use Tabler's funnel for the session chip icon.
Assistant text goes through the markdown renderer, not DirectiveContent, so a session reference an agent wrote came out as literal text. Rewrite bare refs into `#session/<value>` links during markdown preprocessing and dispatch that href to the shared chip in MarkdownLink, alongside the existing media and preview hrefs. Preprocessing already skips code fences and inline code, so a ref being discussed in code stays literal. The pure parsing/href helpers move to session-refs.ts to keep the resolver's React and API imports out of the per-flush preprocess path.
Follow-up cleanup for PR NousResearch#71123: - Extract getattr(args, 'lineage', 'single') == 'logical' to a local (appeared 3x in the export block) - Document that the double _collect_delegate_child_ids traversal in delete_session is an intentional TOCTOU guard inside the write txn
…ck-scope Scope measured-height vars to each chat surface
CuaDriverBackend caches a long-lived cua-driver subprocess for the life of the Hermes process, and stop() was never called from anywhere — the driver outlived the session that spawned it. NousResearch#69903 stopped the orphan from pegging a core by disabling the cursor overlay, but left the process behind; this is item 3 of NousResearch#28152 ("Hermes does not keep the driver alive after tool completion"). Register an atexit hook, mirroring browser_tool's atexit.register(_emergency_cleanup_all_sessions). atexit only, no signal handlers, for the prompt_toolkit reason documented there. reset_backend_for_tests now reuses the same teardown instead of repeating it.
…e-leak fix(sessions): stop a /skill's own text becoming the session title
…eground-leak Render the workspace pane from its own session slice
…tory-cost perf(desktop): make streaming cost independent of transcript length
The sidebar "+" now stacks a tab instead of replacing the surface, so the prior session stays mounted and several chat surfaces can be on the page at once. Helpers that waited for the old transcript to disappear from the page timed out, and `.first()` locators / bare `document.querySelector` calls started resolving against the wrong session (CI's "resolved to 2 elements" strict-mode violation). Target the most recently mounted `[data-composer-target]` surface instead, and assert the NEW surface is empty rather than waiting for the old text to vanish.
An agent told to work in a fresh git worktree does exactly that — creates it, cds in, and runs every later command there — but the session stayed pinned to the checkout it started in. The desktop kept labelling the chat with the primary branch while all the work landed somewhere else. The desktop half already existed: session.info carrying a moved cwd runs followActiveSessionCwd, which refreshes the project tree and scopes the sidebar into the new project. The backend just never reported the move. Reconcile the session's cwd against terminal_tool's per-session record at the end of a turn, when the agent has stopped moving and its recorded cwd is a stable answer. A plain cd stays what it always was — not a workspace move — so the reconcile only fires when the recorded cwd sits in a different git working tree than the session's workspace.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…-atexit fix(computer_use): stop the cua-driver child on exit
Content links read as a small primary-tinted chip instead of an underline, dropping the trailing external-link arrow. The tint is currentColor-relative, so one class carries text and fill in the same hue across every theme, and `box-decoration-break: clone` gives a wrapped link a chip per line fragment.
The arc reduced to a few faint dots on session rows. Two causes: the ring was outset by 2px into a scroller that clips horizontally, losing its left and right runs; and its tail color defaults to the chrome background, which is invisible against the sidebar, leaving only the bright stop of each gradient pass. An `arc-row` variant sits flush and ties the tail back to the ring color. The ring's radius is now derived from its standoff (r_host + gap) rather than inherited, which keeps any outset host concentric instead of pinched.
The strip's rule is an inset shadow painted in the container's last pixel row, and full-height tabs covered it — so each tab read as overhanging the bar by 1px. Inactive tabs compensated with their own border, stacking a second translucent line that darkened the seam. Inactive tabs now stop 1px short and draw no bottom border, leaving the container as the sole owner of one continuous rule; the active tab keeps full height so it alone cuts through. Hover also darkens rather than lightens, since lightening moved a hovered tab toward the active surface's look.
Technical mode rendered the raw payload two different ways — a bare block for most rows, a native `<details>` for file edits, whose browser-drawn marker matches nothing else in the app. Both are now one collapsed chevron disclosure at a smaller type size, with even padding against the row body.
…ollow fix(gateway): follow a session into the worktree it settled in
…-polish Desktop UI polish: link chips, sidebar arc, tab strip rule
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and printed "⚡ loading skill: work" there, then fired the skill's kickoff prompt as a user message into whatever conversation was on screen. The dispatcher resolves its target once, through resolveTargetSessionId, and every other consumer of that answer already honors it: the output writer binds to the target's stored id, and the busy gate reads the target's own state. The send did not — `submitPromptText(message)` passed no target at all, so submit fell back to `activeSessionIdRef`, which names the foreground chat. NousResearch#71805 fixed the two sibling leaks in this same function; this is the third and the one that actually moved the user's prompt. Forward the resolved pair instead. Every target the dispatcher serves — a tile, a background queue drain, a session this very call created — was hitting the same fallback, so the fix covers the class rather than the tab case that surfaced it.
…target fix(desktop): send a skill's kickoff into the tab that invoked it
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…us-tab Open a tab from the sidebar "+" when a chat is already loaded
… the chat
While the workspace pane shows a full page (Artifacts, Skills, Messaging, a
plugin route), a sidebar click on the ACTIVE session did nothing: onResumeSession
took focusOpenSession's `true` for the main-session branch as "already on
screen" and skipped the navigate, but fronting the workspace tab doesn't put the
chat back — the page is still routed. The user had to click some other session
and then the active one to get back.
focusOpenSession now reports WHICH surface it fronted ('main' | 'tile' | null),
and focusedSessionNeedsRoute decides: a tile never needs a route (its pane
renders the chat regardless), a main hit does while a page covers the workspace.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ick-active fix(desktop): clicking the active session from a full page returns to the chat
…t-fix # Conflicts: # apps/desktop/electron/main.ts # apps/desktop/src/app/chat/composer/hooks/use-composer-metrics.ts # apps/desktop/src/app/session/hooks/use-session-actions/index.ts # apps/desktop/src/components/assistant-ui/markdown-text.tsx # apps/desktop/src/types/hermes.ts # gateway/platforms/base.py # tests/test_tui_gateway_server.py # tui_gateway/server.py
Owner
Author
|
Closing as superseded by the merged rebaseline/sync chain (#21 and #36) and the later upstream-sync PR #39. The eight conflict-resolution classes documented here were checked against current |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Unblocks the nightly Nous upstream sync, which has been failing since 2026-07-25 04:00 (
merge conflicts unresolved) and again on 07-26. Merges 513 upstream commits intoace/patches.Why it was stuck
git rererehad only preimages recorded (the nightly aborts before resolving), so every 4 AM run re-hit the same conflicts and failed. It fails safe — the live runtime was never touched — but the fork has been frozen at the Jul 23 baseline.Conflicts resolved (8 files)
tui_gateway/server.py(6 hunks)_claim_update_turn/_release_update_turn_if_idle) — the nightly apply controller depends on it — and took upstream's retained-error snapshot, empty-truncation refusal (confirm_empty_truncate), cancellation emit, and turn-marker retirement. Both sides' helper sets are disjoint, so hunk 1 keeps both.use-session-actions/index.tsresumed.inflight; upstream recovers from a client-side journal. Complementary, not redundant — kept both, gated so the journal wins and the turn can't render twice. Fail-latch keeps upstream's pre-recovery check but does not bail when the server handed us a live turn.types/hermes.tsSessionInflightSnapshotand absorbed upstream's newauto_continuefield pluserror/recoverable/status.markdown-text.tsxelectron/main.tsnative-auth-decisionsimports.use-composer-metrics.tspoppedOutRef(tracks the full gate incl. mobile/coarse-pointer embeds; upstream's inline check covers only secondary windows). Dropped the now-unusedroot.gateway/platforms/base.pyprompt_response) — took upstream.tests/test_tui_gateway_server.pyThree fork-aware test adaptations
test_clarify_timeout_seconds_maps_non_positive_to_unlimited— the fork deliberately removed_clarify_timeout_seconds(protected patch: desktop clarify has no wall-clock timeout).assert replaced == [...]→replaced[0]— Ace's_persist_session_historydurably writes the completed turn at turn end, so the truncation write is the first of two. Upstream has no turn-end persist._CompressingAgent.run_conversationnow absorbs**_kwargs— upstream's fix(desktop): persist @image: refs instead of vision-enrichment text so attachments survive session switch and restart NousResearch/hermes-agent#70720 passes a newpersist_user_messagekwarg that the rigid fake signature rejected.Verification (the nightly's own gates)
capella-patch-guard.sh— 23/23 protected patches presenttsc -b— exit 0, no stray.jsemittedWhy this must land (rather than rely on rerere)
Resolutions are now recorded — the 07-26 run already auto-replayed 6 of them. But adaptations 2 and 3 sit outside any conflict hunk, so rerere can never carry them; the nightly re-derives an un-adapted tree and the gates fail. Landing this puts them in the base permanently.
🤖 Generated with Claude Code