Repository navigation
test(runtime): compare existing-folder imports against the resolved temp path - #33540
Conversation
…emp path On macOS the temp dir sits behind the /private symlink and the runtime stores the resolved path, so two existing-folder tests failed locally. realpath is a no-op on Linux CI.
|
No actionable comments were generated in the recent review.
|
…nest (#187) * docs(fork): record the measured sync cost and the mergeability plan Seven analyses of the 233-path conflict set, the 33-commit patch series, the product line, the gates and the sync tooling, with the figures each rest on. The plan is the deliverable; nothing in it is implemented here. Read-only inputs, no suites run to produce it. * ci(nightly): stop the relay job deadlocking the fork's unit run The relay integration job in unit-tests.yml hard-codes a blacksmith runner label this fork has no runner for. The 2026-10-26 scheduled run sat queued on it for 17 hours and held the run open, and concurrency cancel-in-progress: false then blocked every later night. The fork ships no cloud relay. * chore(lint): drop the max-lines budget Upstream's 300/400/600/800 line budget is what made fork additions evict upstream code into fork files, which is a permanent conflict and a silent-drift risk. The budget is upstream's own code-management construct and the fork does not need it. Proven off with a 320-line probe: passes under this config, errors under upstream's. * fix(gate): an empty branch diff is unknown, never a waiver A sync merges upstream onto nplez1/main, so merge-base(nplez1/main, HEAD) is HEAD itself and the diff came back empty. flakeWaiver reads an empty diff as 'nothing coupled was touched' and waives, so the release gate waived every flaky pass on a line a 556-commit merge had just built. An empty diff is now unresolved: fall back to the merge's first parent, which is what that merge added, and to unknown when there is nothing to compare against. * fix(gate): an allowlist entry can name the failure it covers Entries matched on file and test name only, so an allowlisted test that started failing for a new reason stayed 'known' and was waived. An optional match regex is now tested against the failure's first line, the way lint-gate already works. * fix(quality): resolve the changed-file base from origin/HEAD origin/main is upstream's mirror on this fork, so the pre-handoff gate diffed the fork's whole 2,846-file delta instead of the branch's own. The sibling check-changed-code-quality.mjs already resolves origin/HEAD first. * chore(gate): pin 27 known-red entries to the failure they cover Each of these fills a test name with a red that only its host can produce: bash 3.2 missing mapfile, bash 3.2 refusing an empty array under set -u, macOS /private/var symlinks, and the profile-state SQLite binding. Pinned to the signature the failure text actually carried in the last full run, so the entry covers that reason and not whatever happens to fail under the same title next. The other 13 keep matching on title alone: their observed failure text is a generic deep-equality diff, and guessing a signature there would turn a known red unexpected. * docs(fork): record the two policies, the cap removal and the gate fixes Local-patch rules gain the two policies the analysis argues for: no fork comment in a file upstream owns, and no deleting a file upstream still owns. Both are recorded with the measurement behind them, next to the patches they constrain. Three new patch entries cover what changed here: the max-lines budget is off, the gate now judges a merge by the merge's own diff, and the nightly's relay job is skipped. Each names its anchors and what it does not fix. The ledger's Identity Paths was src/**, which matched 227 of 233 pending conflicts and made 'this path has a decision' true everywhere: now 33, the files that carry an identity literal. The ledger also states the Paths-precision and Resolution rules. * chore(fork): remove fork annotations that sat on unchanged upstream code Applying the no-comments policy to what it actually reaches: of the 18 upstream-owned files whose whole fork diff is comment lines, 12 are the fork rewording upstream comments to match its own reality (pop-out to Agent Dashboard, orca to orca-np, 42 to 49 images). Reverting those would make the comments false, so they stay. The six that were pure annotation of unchanged upstream code are gone: two timeout notes in the win32 PTY test, a doc comment on an upstream ADF builder, a param comment on a watch-path index, a split note in a file the fork did not split, and a paragraph explaining upstream chunk accounting. The lock-path rationale was the one piece worth keeping, so it moved to LOCAL-PATCHES.md rather than being deleted. * feat(nightly): report a stale or wedged backstop instead of leaving it silent The backstop went eighteen days without running and nothing said so. The 2026-10-26 scheduled run queued on a runner label this fork does not provision for seventeen hours, and cancel-in-progress: false blocked every later night behind it. local/check-nightly-liveness.mjs exits 0 on a fresh green run, 1 when the newest run is stale (past two schedule slots) or wedged (queued/in-progress beyond four hours), and 2 when it cannot tell — a missing gh must not read as a green nightly. Six cases in tests/tools/nightly-liveness.test.mjs drive the judgement through an injected clock, and the live run today correctly reports the cancelled run as attention. * docs(plan): record that the gate preflight was dropped on evidence Two of the three proposed checks would have failed a healthy tree. mobile/node_modules does not exist here and the suite is green (pnpm hoists the mobile deps into the root install), and the bun-on-PATH version mismatch 1.4.3 vs pin 1.4.4 is not a hazard because run-vitest.mjs prefers resolvePinnedBunExecutable, which resolves the pin from a devDependency. A check on either would have guarded a path that is not used. The nightly liveness check shipped, as a standalone script rather than a gate step. * feat(sync): give the ledger an executable Resolution field The ledger recorded decisions as prose, so every sync re-derived what to DO with a conflicted path. Local commits in the affected set: zero occurrences of "Resolution:". A section may now carry **Resolution:** with one of keep-ours, take-theirs, delete, rerere, manual, plus a note. parseLedger captures it, ledgerResolution normalises it, and conflicts.mjs prints it above the decision. An unrecognised verb is reported as a ledger defect rather than read as "no instruction" — which is how a malformed entry would otherwise hide. Two sections carry one today: SSH relay removal (take-theirs, upstream retired it) and Identity (keep-ours on the literal, take upstream's surrounding change). 20 fixture tests pass, and the resolver renders "resolve keep-ours" for the nine identity paths in today's merge tree. * chore(fork): restore 13 files upstream still owns instead of deleting them All 39 files the fork deleted between the last merge base and the tip are still alive upstream, so each was a modify/delete conflict on the next sync and a place where upstream fixes were dropped silently rather than surfacing. Thirteen outside the dashboard subsystem come back byte-identical to upstream/main and are recorded as present-but-dead in LOCAL-PATCHES.md. Four stay deleted, with the reason recorded instead of implied: the computer-use Swift pair hardcodes upstream bundle id where this fork trusts its own, and the PR comment resolver needs an ack model #169 deliberately removed. Three restored tests pass (11 + 7 + 9 cases), the restored node-side files typecheck, and nothing in the fork imports any restored module. * fix(test): two orca-runtime failures were test defects, and leave the allowlist Both were recorded as macos-path-semantics environment reds. Both were defects in the tests, so the three allowlist entries go in the same commit rather than being widened. The /private/var mismatch is upstream #30657 missing two cases; upstream fixed it in #33540 and this takes that hunk byte-for-byte, comment included, so the next sync has no hunk to resolve. The unhandled selector_not_found is the test attaching .rejects after awaiting a different promise, so the rejection settles unobserved and Node reports it while the assertion still passes; the allowlist evidence blamed the coalescing map, which the stack trace contradicts. pnpm test src/main/runtime/orca-runtime.test.ts: before 2 failed + 1 unhandled error, after 1520 passed | 3 skipped, green on three consecutive runs. * fix(test): the four orcad forked-child cases were the same /private/var defect They set process.argv[1] to an unresolved mkdtempSync(tmpdir()) path while the resolver reports the resolved deploy root, so all four compared /var against /private/var. Each resolves its temp dir first now, and all five cases in the file pass. Upstream has not fixed this one, so it is a fork-local test fix that will conflict once, benignly, when upstream makes the same change. Four entries leave the allowlist with it: 38 entries becomes 34, and the macos-path-semantics class drops from 7 to 3. * fix(test): the three remaining macos-path-semantics reds were the same bug Each created a temp dir with mkdtemp(tmpdir()) and compared it against a path the subject had resolved: the darwin launcher's `cd -P`, and a spawned child's reported cwd. Each now resolves its temp dir first. All three files are otherwise byte-identical to upstream/main, so this is a fork-local test fix that will conflict once, benignly, when upstream makes the same change. With these, the macos-path-semantics class is empty: allowlist 40 entries down to 31, and the remaining 22 + 9 are host-toolchain and sqlite-binding, which are this machine's bun and SQLite rather than anything a test can fix. * fix(daemon): the sync merge rewrote two fixtures' imports to file-relative paths Both fixtures bundle an inline script with resolveDir = process.cwd(), the repo root, so their imports must be root-relative. Merge 281cccf5a4 rewrote them to file-relative paths that exist in neither merge parent, so esbuild failed with "Could not resolve" and the affected electron suites reported a failed file with every test skipped. The daemon-launcher-driver fixture is byte-identical to upstream/main again. The orcad adoption test is latent because CI skips it for missing inputs; its three rewritten imports are the same regression and are fixed the same way. * fix(test): the cooldown 401 expectation is per-platform, not macOS-only #134 removed the fixture's refresh token and changed upstream's stale-token expectation to the value macOS produces. On darwin the exchange cannot re-read the Keychain item, so the host reports refreshable-credentials-without-token; elsewhere it reads the credentials file, finds no refresh token, and reports stale-token. Both are correct for their credential store, so the expectation now depends on the platform. 12 cases pass on macOS, which takes the darwin branch unchanged. * fix(ephemeral-vm): observe stdin errors so a fast child exit cannot crash the host A recipe that exits without reading stdin, as the context-menu test's `exit 9` does, makes the payload write fail EPIPE. Nothing observed the error on child.stdin, so it became an uncaught exception: an unhandled-error headline in the Linux nightly shard 3, and in production a crash of the Electron main process. The exit code already reports the outcome, so the listener is deliberately silent. Upstream has the same line, so this is fork-local until it goes upstream. src/shared is a global input, so a PR carrying it needs the release gate. * feat(ci): name the file a shard verdict could not blame A file whose import or beforeAll throws reports every test skipped, so no failing case names it and the shard reads as red with no culprit. The verdict now adds a line for a failed file that has no failing test. Replaying the five shard artifacts from run 40308443374 produces the expected output, and 14 tests pass across the two suites. * docs(sync): check what the merge resolution itself invented Every post-sync check asks whether the tree is coherent; none asks whether the resolver wrote something neither parent said. The 2026-10-25 merge turned './src/main/daemon/daemon-spawner' into './daemon-spawner' in two esbuild fixtures, and because the shorter path still resolves relative to the changed file, no import check complained. Inside an inline script bundled with resolveDir: process.cwd() only the root-relative form exists, so the electron suites died in beforeAll and the shard verdict named no culprit. The runbook now points at git show --remerge-diff for exactly those hunks, and at the sites where resolveDir makes the difference matter. Both are verified against the merge that caused it. * feat(sync): a manifest that answers a modify/delete conflict modify/delete is the one conflict class where restoring the file is the tempting wrong answer, and the resolver printed it as a bare conflict with no answer, so a human re-decided it every sync. local/sync/expected-deletions.json now holds the 24 paths this fork deletes that upstream still owns, each with the commit that deleted it and why, under one of three statuses: expected-deletion (re-apply it), restore (the file comes back), or pending-decision (nobody has chosen). 20 of the 24 are the dashboard/pop-out set, which is a pending product decision, and the triage note names those paths so they cannot be resolved by accident. Four are settled with reasons recorded. The manifest is checked against HEAD on read, so one that still calls a restored file deleted is reported as stale rather than trusted. Seven fixture tests cover the reader; the live triage prints the status, the reason and the ledger entry for a conflicted path. * fix(quality): a restored upstream file is not the fork's to fix Restoring use-jira-issue-workspace-data.ts byte-identical to upstream tripped three no-adjust-state-on-prop-change findings, because a restored file counts as all-new lines and the rule is the fork's own. Fixing upstream's code here would be divergence the next sync re-resolves, so the file joins the existing suppression map with that reason. This is the same class a sync hits at scale: every upstream file a merge brings in is "new" to this gate while being byte-identical to upstream. * feat(sync): propose and apply the resolutions the fork has already settled The ledger recorded decisions as prose and the deletion manifest recorded statuses, but nothing turned either into an action, so a sync still asked a human about every path. --propose now prints what is settled twice over, and --apply runs the git steps and stages them without committing. A verb is a whole-file decision, which exposed a real hazard in my own ledger entry: the Identity resolution said "keep-ours on the literal, take upstream's surroundings", and a whole-file keep-ours would revert upstream's work wholesale across 33 paths. That entry is now manual, with the reason, and the propose engine returns nothing for it — 3 of 231 conflicted paths carry a settled resolution today, all from the deletion manifest, and the rest are honestly reported as manual. Verbs written as prose now name their verb (manual. is manual). 42 fixture tests cover the readers; --apply refuses a --merge-tree run because there is no index to resolve. * docs(plan): record what landed, what moved and what is still open The plan was written as analysis and said so. A session reading it now would re-derive decisions already made and re-litigate items already dropped, so it opens with a status section instead: the fourteen changes that shipped, the six corrections to its own premises (preflight dropped, comment sweep narrower than claimed, exit-2-is-not-a-pass), and the four items still open with the dashboard decision named as the blocker. Baselines that moved are marked rather than rewritten, so the measurement in section 1 stays comparable. * fix(sync): two deletions the manifest missed, because a merge deleted them agent-dashboard-open-mode.ts and its test are gone from HEAD and alive upstream, but the manifest was generated from git log --diff-filter=D, which cannot see a file a merge resolution removed without a commit of its own. Generated from git diff --diff-filter=D instead: 26 deleted paths, 26 manifest entries, nothing uncovered. Both are dashboard-delivery, so they join the pending decision rather than settling it. * refactor(fork): put six upstream blocks back where upstream has them The max-lines cap made the fork carve cohesive units out of upstream-owned files into fork-owned siblings. Each carve is a permanent conflict at that point plus a copy that drifts, and with the cap off the split buys nothing. Six blocks that were upstream's code and nothing else go back verbatim from the merge base, and the siblings are deleted: tooltip.tsx <- provider-rate-limit-window-sections.tsx CheckRunDetailsPanel.tsx <- check-run-details-status.ts web-filesystem-api.ts <- web-file-mutation-session.ts hook-settings.ts + hook-service <- claude-status-line-slot.ts accounts-search.ts <- accounts-search-extra-providers.ts filesystem-watcher-wsl.ts <- filesystem-watcher-wsl-snapshot.ts Verified: tc:web and tc:node clean, and the suites covering each — provider-panel-rendering 34, hook-service 21, web-filesystem-api-connection-pin 2, CheckRunDetailsPanel 37, accounts-search 15, filesystem-watcher-wsl 16. Thirty-one other splits were measured and left alone: the block carries fork code, was rewritten rather than moved, or upstream has changed it since. The ledger's accounts-search decision is updated because this reverses the reason it recorded, and a dated addendum lists the six so the older chronicles are not rewritten. * docs(plan): six of the 37 measured cap splits are undone * fix(test): install bash 4+, which clears fourteen allowlisted failures Every one of these was a host-toolchain entry whose cause was Apple's /bin/bash 3.2: six cases across ci-managed-ssh-execution and dependency-security-release die on `mapfile: command not found`, and eight pr-auxiliary-preflight-admission cases hit `MOBILE_CI_ARGS[@]: unbound variable` because bash 3.2 rejects an empty array under `set -u`. The ubuntu runners ship bash 5, so these never failed for upstream. Naming the cause rather than the symptom is what made it a one-command fix: homebrew bash 5.3.20 now wins resolution, because /opt/homebrew/bin precedes /bin on this machine's PATH. No repository change, no wrapper. The three files run 233 passed. Allowlist 31 entries / 13 files -> 17 / 10. What remains is bun and SQLite behaviour (host-toolchain 8, sqlite-binding 9), which is a different machine's problem to fix, not this fork's. Pruning moves the entry count without a new full run, so the manifest's arithmetic stopped closing: observedRun describes the ef2897da2b run, which had 40 allowlisted. Rather than repointing that number at a run nobody did, every entry pruned since it is recorded in prunedSince with its class, reason and the revision that fixed it — 23, the earlier nine macos-path-semantics ones included — and the guard now closes over entries + prunedSince, requiring both fields on each. * fix(sync): restore the popout plumbing, assert the part that cannot be inert The dashboard/popout subsystem was 22 modify/delete conflicts waiting on every sync, because the fork deleted files upstream still owns and still edits. The restore now splits where inertness actually stops, and the split is recorded rather than implied. Restored, byte-identical to upstream/main (7): src/main/ipc/dashboard-popout.ts, its test, src/main/window/dashboard-popout-window.ts, its test, clipboard-dashboard-popout-access.test.ts, src/preload/api/dashboard-api.ts and dashboard-bridge.ts. Nothing registers the IPC or opens the window, so they are dead — but they typecheck and all 33 of their cases pass, which makes holding upstream's version free. Restoring them needed `dashboard: DashboardApi` back in PreloadApi and upstream's isDashboardPopoutRenderer admission in assertTrustedClipboardTextSender — text authority only, never images, files or remote operations. Left deleted, asserted in expected-deletions.json (15): the drawer, the sidebar host, the popout window shell and the open-mode/bridge hooks. They cannot be restored inertly. Upstream's drawer needs AppState.agentDashboardDrawerOpen, which #44 removed, and upstream's DashboardPopoutRoot needs the onAckAgent/onRevealAgent props upstream added to AgentKanbanBoard — the component the fork's live page view renders. Restoring them means two live dashboards or a behaviour change to the shipped one, which is a product decision, not a merge one. Restoring all 22 was measured before deciding: 11 of 57 cases failed and tc:web reported 29 errors, because upstream's renderer half has moved past the fork's. electron.vite.config.ts keeps upstream's popout entry deleted along with popout.html: entry that resolves nothing does not build. The three restored suites trip the changed-lines gate on their own `as` casts, which read as entirely added lines until this branch merges, so the suppression map — renamed to say what it now holds — covers them for the same reason it already covers a restored Jira file. tc:node and tc:web clean; 33 restored cases pass; allowlist untouched at 17. * docs(sync): the dashboard view is the delivery, so stop offering a restore The owner rejected upstream's drawer and pop-out window outright: the fork's content-area view is the dashboard, and the overlay/pop-out delivery is a different feature, not a better version of the same one. That settles what was still an open question in the ledger, the manifest and the plan — and it makes three of the seven files the last commit restored a liability rather than ballast. Dropped from the restore (3): src/preload/api/dashboard-api.ts, src/preload/api/dashboard-bridge.ts and clipboard-dashboard-popout-access.test.ts. The preload pair exists only to publish window.api.dashboard, which the fork's board stopped calling when it dropped the pop-out relay; dashboard-bridge.ts type-checks itself against PreloadApi['dashboard'], so holding it forces a `dashboard` member onto PreloadApi for a runtime API nothing registers — the type lie would be load-bearing. The clipboard test went with them because it asserts that assertTrustedClipboardTextSender admits upstream's pop-out renderer, and the last commit had wired exactly that admission in: dead code adding a branch to a clipboard trust check is a live behaviour change for a feature nobody can reach. All three wiring edits are reverted, so the 4 held files now import nothing of the fork's and nothing imports them. Recorded in three places instead of one, because the resolutions differ and a Resolution: has to be executable: the 18 renderer/preload deletions are `delete`, the 4 inert main-process files are `take-theirs` (byte-identical to upstream, so a sync should not read them), and AgentKanbanBoard.tsx is `manual` — 14 code hunks of fork behaviour that keep-ours would revert upstream's unrelated board work on and take-theirs would revert your click behaviour on. The resolver now proposes 14 deletions that all agree with the manifest, with no ledger entry unmatched and no manifest entry unledgered. Also adds the missing entry for pr-comment-snapshotted-thread-resolver, which was a settled deletion with no ledger record. Comments that name the pop-out stay, in eleven files: each one exists because the surface it described no longer exists, so restoring upstream's text would reintroduce a false statement. That is checked against the merge base, not against upstream/main — diffing the latter reports upstream's own unmerged progress as if it were fork divergence, which is how the same scan first reported 1741 comment hunks. Plan § 5.4 is reversed in place with the measurement that supports it: restoring all 22 leaves 11 of 57 cases failing and tc:web at 29 errors. 49 cases pass across the board and the two held suites; tc:node and tc:web clean; allowlist untouched at 17. * fix(tools): a cancelled nightly is not a red one, and must not hide the last verdict The liveness check reported "nightly red (cancelled) at 19.1h old" — about a run I cancelled by hand. Two defects in one reading: a cancelled run carries no verdict at all, so calling it red is a false alarm; and because the judge sorted every run by recency, that cancelled run also masked the last run that actually passed. A backstop checker that cries wolf on a deliberate cancel is exactly the nightly nobody reads. It now keeps two clocks. The schedule's clock asks "does any run exist recently", because a cancelled run still proves the cron fires. The backstop's clock asks "has a completed run produced a verdict recently", because only that proves the suite runs. Cancelled, skipped and neutral runs advance the first and are named in the summary rather than counted as a verdict, so the two ways the backstop can be down — not firing, and firing without ever finishing — read differently instead of both reading "red". What it says now, which is the reason this mattered: the newest verdict is 450.1h old and green. The fork has had no passing nightly since 2026-10-08; every run since is either the wedged one or my cancel of it, because the relay fix on this branch is not pushed. That is the honest state of #13, and the old judge could not report it. Six cases added (10 total): cancelled-only, cancelled-over-green, days of cancellations, and a schedule that fires while the last verdict is stale. * docs(plan): §0 said seven files restored; it is four Today's decision changed the dashboard split and the liveness checker, and §0 is the section a later session reads to avoid re-deriving both, so it has to be true. Four files held inert, eighteen asserted deleted, the two reasons the preload pair did not survive, and the measurement behind the reversal. The baselines that moved were stale in the same way: 26 deletions rather than 24, and 15 conflicted paths with a settled answer rather than 3 — with the new row that matters most, that the last passing nightly is still 2026-10-08 at 450h, because the fix is unpushed. * refactor(fork): delete four fork-only modules nothing imports §4.9 of the plan, open since the ledger named three of these dead on 2026-10-15. All four are fork-created (absent from upstream/main, so the no-delete policy does not apply) and all four have zero importers: src/renderer/src/components/task-page-jira-board-page-state.ts 36 src/shared/quick-open-path-inventory-limits.ts 26 src/shared/workspace-path-search-request-budget.ts 23 src/shared/skill-freshness.ts (+ its test) 205 skill-freshness is the one that needed a second look: the names SkillBundleFileIdentity and SkillUpdateRun appear in other files, but those declare their own copies — a textual scan for the names says "used", a scan for `from '.../skill-freshness'` says the module's own test is its only importer. Its both-owner lock-source rule had stopped being on a live path after the merge, so the ledger's "wire it in or delete it" resolves as delete rather than as a policy the app no longer applies. check:test-file-runners failed with ENOENT until the deletions were staged, because it enumerates git ls-files rather than the working tree — worth knowing before believing that failure on any other delete. tc:node and tc:web clean, test-file-runners and max-lines ratchet pass, and the ledger plus LOCAL-PATCHES now record the deletion instead of the module. * chore(sync): retire curate.mjs and stop describing a patch series that cannot exist §6.9. Three things the docs promised and the repo did not have. curate.mjs is deleted rather than finished. It planned a curated, linear patch series for the rebase era, counting from `git log merge-base..HEAD` without --first-parent — 332 commits including merges — so the per-group byte-identical promise never held on non-linear history, and it was never applied. The series it planned cannot be cut at all: no patch ref exists, the product line imports the identity modules across ~50 commits, and 35 files are shared. Finishing the tool would have bought a plan for a branch nobody can build. The "keep rebase for the patch series" wording in the runbook, LOCAL-PATCHES.md and local/sync/README.md now says what is true: the release line merges, feature branches rebase, and there is no patch series. The 2026-10-05 decision itself stands — only its object was imaginary. .husky/commit-msg is scoped to lineage instead of to a branch name. It refused `local(` everywhere but nplez1/main, fork PR branches included, and the measured result was worse than the mistake it guarded: policy work committed there without the local( label at least five times (#146, #130, #142, #177, #52), so an unlabelled fork-only change rode a fork PR rather than being refused. It now allows local( on any branch carrying nplez1/main in its history and still refuses anything cut from origin/main — verified both ways: this branch accepts a local( subject (exit 0), and `git merge-base --is-ancestor nplez1/main origin/main` is false (exit 1), so an upstream-bound branch still trips it. open-pr.mjs --upstream stays the second stop. * fix(gate): key the verdict cache to the repository, so two worktrees run the suite once §6.8's first item. Both readers — `gate-unit.mjs` writing the verdict, `open-pr.mjs` refusing a PR without one — resolved the cache from `git rev-parse --absolute-git-dir`, which answers with the *worktree's* git dir (`.git/worktrees/<name>`). So a revision gated in one worktree was invisible to the other, and the same SHA paid the full suite twice; at 25 minutes a release gate that is the most expensive duplicate in the workflow. `resolvedGateCacheDir` now prefers `--git-common-dir` (absolute, with a relative answer resolved against the repo root), falls back to the worktree git dir, and only then to `<root>/.git`. The cache entries were already keyed by revision, allowlist fingerprint and gate inputs, so the shared directory cannot hand back a verdict that belongs to another revision. `unitGateVerdict` infers the git dirs only when the caller supplies neither: an explicit `gitDir` is an answer, and inventing a common dir over it would read a cache that caller did not choose — which is exactly what the existing open-pr tests do. Three cases added: the worktree and the main tree resolve to one directory, the fallback when no common dir is reported, and the relative-path case. 30 pass in that file, 49 across both gate files. * docs(plan): the node-runtime pool caveat is wrong, and §6.8 records what changed The caveat said the fork's custom pool "never consults maxWorkers, so that project is not covered by the cap". It never had to: vitest 5.0.5 wraps any custom poolRunner in the same PoolRunner whose schedule() returns early at activeTasks.length >= this.maxWorkers, setMaxWorkers is applied per task group, and VITEST_MAX_WORKERS becomes resolved.maxWorkers. The fork's pool spawns exactly one child per createPoolWorker with no internal fan-out, so capping workers does cap processes. Recorded rather than corrected everywhere: the sentence a reader acts on lives in the machine-level AGENTS.md, which is outside this repository and is the user's directive file. One empirical confirmation on an idle machine should precede changing it. Also records the verdict-cache item as done, in the same commit that implemented it. * feat(sync): report fork-only modules nothing imports The ledger named three dead modules on 2026-10-15 and they were still there on 2026-10-27. That is the failure this closes: the sync cannot see a file upstream does not have, so a fork module whose last caller an upstream change removed simply stops being read, and nothing anywhere says so. `unreferenced-fork-modules` scans every fork-only path under src/ (no tests, no fixtures) against the specifiers that every tracked source file carries — including the `@/` and `@renderer/` aliases — and reports the ones nothing resolves to. Report-only: deleting product code is a judgement, and the check's job is to make the judgement cheap. 1.5 s on this tree. Two limits are stated rather than hidden. A cycle of two files that import only each other reads as live; distinguishing that from a genuine pair needs reachability from an entry point, which the repo does not have. And a file loaded by path — `workspace-path-index-benchmark-worker-entry.ts` is the example — is invisible to an import scan, which is what the allowlist argument is for. Its first run, mid-flight for five agents, named eight candidates; several are their in-progress edits, so the triage belongs after the tree settles. 53 sync-lib tests pass, 11 of them new. * refactor(config): give the two count ratchets a fork overlay §4.8. `focus-setting-read-baseline.txt` and `flaky-test-pattern-baseline.txt` are per-file sorted monotonic counters, so both sides bump them and they conflict on adjacent lines every sync. Each now has a fork-owned overlay that records only the rows whose count differs, including a count of 0 for "the fork fixed this while upstream still allows it". `readCountBaseline` merges base then overlay per key; `pruneCountBaseline` writes the overlay when one exists and never upstream's file, which is the half that matters — a prune that edits upstream's baseline would reintroduce the conflict silently. Both baselines are restored to the merge base's bytes, not to `upstream/main`'s current ones. That distinction is the point: aligning to upstream's tip would need a 0-row for every file upstream has added since, and those rows would fail the moment a sync brings the file in. Restoring to the merge base leaves the fork with no delta at all, so the next sync takes upstream's version cleanly. Neither checker moved: 690 reads and 3,462 flaky findings, before and after. 14 tests in ratchet-baseline.test.mjs, 5 of them new: overlay overrides base, a 0 is a real override rather than "absent", no overlay means unchanged, and prune writes the overlay while leaving the base untouched. * refactor(fork): put the TUI agent table back, keep one overlay entry §5.5. The fork's `tui-agent-config-table.ts` was upstream's 361-line table plus one entry, copied into a fork-owned file. A copy does not conflict when upstream edits the table — it silently diverges, which is worse: the fork's copy keeps answering for a table upstream has since changed. The table is back as upstream's file (from the merge base, so the next sync merges it), and the fork's one entry lives in `tui-agent-config-np.ts` behind `withForkTuiAgentOverrides`. Net delta in upstream's file: 5 insertions, 4 deletions — the wrap, the remote-platform launch lookup, and the comment saying why. The spec hoped for two; the third line is the lookup the fork actually needs, and squeezing it into the wrap would have made both harder to read. Behaviour is unchanged, and proved rather than asserted: every agent's resolved config and launch command was dumped on darwin, linux and win32, local and remote, before and after — identical except the key order inside the Agent Teams entry. Two tests now cover the overlay entry and the remote rule per platform. 5/5, 3/3 and 8 passed | 1 skipped across the three suites that touch the table. `tui-agent-config-types.ts` stays and keeps its fork field: it is upstream's own file at that path, and deleting it would collide. * refactor(i18n): the fork's English strings move to an overlay §5.1. The fork satisfied the single-source rule by writing 722 keys into upstream's `locales/en.json`, so that file and the derived `en-runtime-required.json` conflicted on every sync — about 20 paths, on files whose content is entirely machine-checkable. `src/renderer/src/i18n/fork/en.json` now holds the fork's added and overridden keys (736), with `fork/en-runtime-required.json` derived from it (204 entries: what upstream's subset leaves out or contradicts). `config/scripts/fork-english-catalog.mjs` merges overlay onto upstream and routes new keys into the overlay, so `--fix` can no longer write into upstream's file. Each read site is a one-line swap: both verifiers, the renderer-string localizer, and the runtime-catalog generator. At runtime `i18n.ts` adds the overlay bundle after init. Both upstream files are byte-identical to the merge base, not to `upstream/main`: matching the current tip would need 26 more overlay keys for entries upstream has since deleted (17) or reworded (9), and after the next sync those would keep the old wording over upstream's new text. Our side equal to the base is what makes the next sync take upstream's file as-is. Non-English locales stay untouched, so the fork's eight translations there still apply. All four verifiers pass (catalogs, extraction, coverage, runtime catalog); 11 mirror tests and the runtime-generator and dev-channel suites pass; tc:web clean. * refactor(build): the Orca NP identity moves into a builder overlay §5.8. `config/electron-builder.config.cjs` is upstream's file and upstream commits to it about 41 times a month; the fork carried 45 added and 21 removed lines in it for identity alone, so every one of those commits collided. Upstream already uses the pattern — `config/electron-builder-pr-linux.config.cjs` spreads the release config and overrides it — so `config/electron-builder.np.config.cjs` now does the same for appId, productName, protocols, win.executableName, extraResources CLI names, deb/rpm packageName and the build patch's publisherName/verifyUpdateCodeSignature. The base config is upstream's text except `ORCA_MAC_TEAM_ID`: that is a module constant read inside `afterPack`, so it cannot travel in a config the patch loads later. +6/−2, down from +45/−21. Six entry points now name the overlay — `fork-release.yml` (mac, win, linux), `build-mac-local.mjs`, `build-linux-local.mjs`, and `build:unpack` / `build:win` / `build:mac:release`. That adds three fork hunks to `package.json` in exchange for sixty in the builder config, which is the better trade: package json is edited far less often, and a new test fails if any of those six points back at upstream's config, so a sync that takes upstream's side of a build line cannot silently ship as "Orca". The effective config is unchanged, proved rather than asserted: serialized with sorted keys and hashed function sources across seven env sets (default, the three platforms' release and hourly variants, and a local build version), byte-identical before and after. `config/scripts/electron-builder-np-config.test.mjs` pins it — 7 cases, and it fails if the overlay changes any field outside the identity list. `check-upstream-identity-literals` moves out of `package.json`'s single `lint` line (100 upstream commits in 30 days) into `local/gate-lint.mjs` as a fork-only step. It cannot silently stop: the gate throws when the script is missing instead of skipping, `local/gate-lint.mjs` is fork-owned so a sync cannot touch it, and `tests/tools/lint-gate.test.mjs` asserts the real repo resolves the step. `test:skill-sharing:release` now runs through `run-vitest.mjs`; `test:node` deliberately does not, and that is recorded with its reason. * refactor(fork): three upstream seams stop being copies §5.6, three of four. Each was a place where the fork copied upstream's lines to insert a few of its own, which is a conflict on every sync plus a copy that drifts silently. **`worktree.remove`** copied ~55 upstream lines into `worktree-remove-method.ts` to add one flag. The flag now travels through `queueWorkspaceDelete`'s `run` argument from `workspace-lifecycle-commands.ts`, `worktree.ts` is byte-identical to the merge base again, and the copy is deleted. A test asserts the flag actually reaches the removal rather than that a function was called. **`orcad-entry.ts`** evicted upstream's `prepareOrcadSecretStoreAndMigrate` to buy four lines of `setScreenPromptReader`. Upstream's function is restored byte-identical, `wireScreenPromptReader` is exported from the existing `server-screen-prompt-wiring.ts` and called from both hosts, and the fork's `orcad-secret-store-bootstrap.ts` is deleted. The entry's delta drops from +7/−18 to +3/0 and its next-sync conflict count to zero — and because upstream has since moved that function into `orcad-secret-store-startup.ts`, the move now merges cleanly instead of colliding. **`agent-hook-listener.ts`** evicted upstream's SessionEnd and inherited-owner blocks. Both are restored byte-identical and the two fork helper files are deleted; the delta drops from +62/−67 to +45/−7 and next-sync conflicts from 2 to 0, so upstream's new `incomingSession` argument lands silently. Not done, and the reason is worth keeping: the fork's pre-admit has to run *after* upstream's envelope, OpenCode and compact steps and *before* the inherited-owner check, which a wrapper outside the function cannot reach without copying those steps — and the post-gate reads internals the function never returns. The guard calls themselves are byte-unchanged and in the same position; only the code around them moved, so the ledger's "the fork's guards stay" decision is untouched. Settings types: the fork-only fields move into `NpSettings` (which `GlobalSettings` already ends with), upstream's task-view fields go back into upstream's type, and the fork-only `task-view-settings-types.ts` is deleted. Verified: tc:node and tc:web clean; 9, 34, 21, 6, 17 and 10 cases pass across the suites covering the six touched files; every deleted path was confirmed fork-only, since deleting a file upstream owns is the one thing this policy forbids. * docs(plan): seven items landed today, and §0 is the record §4.8, §4.9, §5.1, §5.5, §5.6 (three of four seams), §5.8 and §6.9 are done, and the nightly unblock is PR #186. §0 lists each with the learning worth keeping, because several of them produced one: - The merge base, not `upstream/main`, is the right restore target for a file both sides edit — aligning to the tip needs a 0-row for every file upstream has added since, and those rows fail the moment a sync brings the file in. - `ORCA_MAC_TEAM_ID` cannot move into the builder overlay: `afterPack` reads the module constant. - The hook listener's decorator does not fit. The fork's pre-admit runs between upstream's compact steps and its inherited-owner check, and a wrapper outside the function cannot get there without copying them, which is the copy this plan exists to remove. - The liveness check's own first version proved the point: a cancelled run is not a verdict, and an in-flight run may never stand in for one. "Still open" now names what is actually untouched — §5.3, §5.7, the other §6 items — rather than a range that today's work made false. * fix(build): keep upstream's cast, and suppress only its own finding Review caught two casts my §5.5 commit added; removing them was wrong for one of them. `Object.fromEntries` returns `{ [k: string]: TuiAgentConfig }`, which is not assignable to `Record<TuiAgent, …>` — which is why upstream itself writes `as Record<TuiAgent, TuiAgentConfig>` at that exact spot. The seam only works if that line is upstream's, so it is restored, and the changed-lines gate is told why the finding appears: our one-line wrap makes the whole statement a changed hunk, so a cast that exists at the merge base reads as new until the branch merges. The other cast was mine to remove and is gone: `withForkTuiAgentOverrides` no longer casts an object key to `TuiAgent`, because `satisfies` keeps the literal key type and the merge then indexes a typed record directly. Worth recording, because it would have failed quietly: the suppression map is a `Map`, so a second entry under the same key replaces the first. My initial fix added a duplicate `typescript(consistent-type-assertions)` key, which silently dropped the three restored dashboard suites from the suppression list. It is now appended to the existing set instead, and a duplicate-key scan over the map reports none. tc:node and tc:web clean; the gate passes across 88 changed files; 5/5 in tui-agent-config.test.ts. * revert(build): test:skill-sharing:release is upstream's, and the bare form works An agent rewired this script through `run-vitest.mjs` on my instruction, to "remove the vitest-direct trap". The premise was wrong twice over, and the measurement settles both. Both scripts the plan named — `test:node` and `test:skill-sharing:release` — are **upstream's**, byte for byte. `test:node` exists to run the suite on Node rather than Bun, so routing it through `run-vitest.mjs` (which always launches pinned Bun) would delete its reason to exist; the agent declined that one and was right. The other script's bare form was then measured instead of reasoned about: 648 passed, 44 skipped, no failures, so the harness env it "skips" costs nothing on the scopes that script covers. Which leaves a fork hunk in upstream's package.json that buys no behaviour, on a file upstream commits to often. Reverted to upstream's text. The residual defect is real but upstream's: a bare `vitest` over the *whole* suite misses `ORCA_BACKGROUND_LAUNCH=1`, so the browser-manager suites red — that belongs in the upstream-worthy list (§6.3), not in a permanent fork patch. Plan §6.8 corrected in the same commit: the item is closed as not-a-trap, with the measurement. * docs(nightly): the red set was the branch gap, and the third failure was a test defect The 2026-10-27 dispatch is a cleaner measurement than the cancelled 2026-10-26 run, and it says something the entry did not: Linux carried no environment-bound red set of its own. Three shards were green, and the three red files map one-to-one onto commits that exist here and not on nplez1/main — the cooldown expectation, the fixture's esbuild specifiers, and the unhandled selector_not_found. That last one was wrong as recorded. fbb41b497d established it was a test defect, not a Linux-only reproduction: the test attached .rejects after awaiting a different promise, so the refusal settled unobserved while the assertion still passed. The claim that the allowlist covered it is withdrawn. Recorded with the run id and the three commits because the next reader's question is "is the nightly red on main, or is my worktree ahead of main", and only the commit list answers it. * feat(sync): a fork-surface smoke selector, and the measurement that resizes it Plan §6.1 wanted a cheap post-sync run over the tests the fork's own patches touch. The selector is built (local/sync/smoke-selector.mjs over a pure local/sync/lib/smoke-select.mjs, 10 fixture tests) and it reproduces the plan's 954 fork-touched test files exactly. It also overturns the plan's cost claim. On merge 281cccf5a4, at the plan's own 1.0 s/file rate: touched-only is 886 files / 14.8 test-minutes (~7 min at 2 workers); the one-import-level walk is 4,459 files / 74.6 test-minutes (~37 min) — within 15% of the 43-minute full gate it was meant to replace. The "526 files / 8.8 test-minutes" figure is not reconstructable from the tree. §6.1 now records the table instead of the claim, and the cheap mode is an explicit --touched-only flag rather than the default. The tool also refuses to pretend: it names the touched tests it cannot run, and reports shared harness inputs in the diff instead of silently expanding to the whole suite. Stream parsing reads the whole batch response before walking it, so the buffer is concatenated once rather than per chunk. * refactor(renderer): the fork's usage providers as data, not five if-arms The fork threads deepseek/fireworks/copilot through upstream's status-bar chains: five separate places in status-bar-provider-visibility.ts, plus the segment, tooltip, error-copy and an 85-line append to icons.tsx. Every upstream commit in that area conflicted, and the ledger calls this the single most repeated conflict shape in the repo. Now one mapping file answers all of it. icons.tsx is byte-identical to the merge base, the duplicate hasUsageData in StatusBarProviderSegment.tsx is gone, fork fields join the upstream types with one "&" each, and every guard is a single mapping consult. Measured honestly, and the premise only half holds: hunk counts are nearly unchanged (visibility 10->10, segment 6->7, tooltip 7->7, error-copy 5->6, menu 2->2) because a guard at a call site still diffs at that call site. What changed is hunk size — 1-2 lines instead of 3-9 — and the largest single fork block in the renderer is gone. §5.3 records both numbers rather than only the flattering one. Also fixes a quality-gate finding in the smoke selector: interpolating an inferred-null field in a template literal is rejected as an untyped expression. tc:web clean; 89 tests across five files. * docs(plan): §0 records the renderer half, the selector, and what the nightly measured Four rows: the usage-provider mapping with its honest hunk-count finding (cheaper conflicts, not fewer), the smoke selector and the cost claim it overturned, the nightly red set read as the branch gap rather than Linux environment classes, and PR #186's squash — which is why a PR body has to carry the story here. Also joined four table rows that had blank lines between them, which split one table into four. * refactor(rate-limits): the fork's three providers ride upstream's sibling lane The fork threaded deepseek/fireworks/copilot through upstream's positional provider tuple, so every upstream commit touching that tuple conflicted and the merge had to re-resolve indentation. Upstream already runs grok/cursor/zcode/antigravity outside the tuple via settleSiblingProviderResult; the fork now does the same through rate-limits/fork/fork-provider-cycle.ts. The point of the seam is measurable and measured: the 50-line Promise.allSettled destructure and tuple in service-full-cycle-preparation.ts is now BYTE-IDENTICAL to 0571e45107 (verified with diff), that file's fork hunks went 11 -> 8, and it lost 60 net lines. The disabled gate moved to the injected resolver boundary and is applied once in updateState, which let fetchUnlessDisabled be deleted rather than maintained. Two tests: sibling settle order (the fork chip is fetching mid-cycle and ok after) and a disabled provider reading nothing (resolvers, gh probe and fetchers all uncalled, slots null). Caveat recorded in the plan rather than hidden: with the tuple verbatim, a disabled opencode-go still enters upstream's fetchOpenCodeGoUsage with an empty config, so its env probe runs and its resolver does not. That is upstream's early return, not a fork credential read -- gemini-usage-fetcher.ts:221 returns before any OAuth source -- and the pinned test now asserts the flag is passed rather than that the fetcher is never entered. Keeping "never entered" would have meant keeping a fork hunk in the tuple. tc:node clean; 17 single-file test runs green; changed-code gate passed. * fix(test): the vm-recipe mocks passed a stdin that cannot emit errors 4bb73dcb45 observes stdin errors so a shell exiting before reading stdin cannot crash the host with EPIPE. The three fake children in index-vm-recipe-doctor.test.ts declare stdin as { write, end } -- but the production type is ChildProcessWithoutNullStreams, whose stdin is an EventEmitter. So the mocks were a lie about the type they stand in for, and the fix in production surfaced it: "child.stdin.on is not a function", plus a cascading expect(undefined) in the provision-failure case. Making stdin an EventEmitter like its stdout and stderr siblings keeps the fix in production where the bug was, rather than guarding it for the benefit of a mock. 4/4 pass; the two cases were red on the gate run before this. * revert(build): enforce upstream's max-lines budget, and exempt the eight files instead Reverses 1ba1086499 with the measurement that reverses it. Setting max-lines to "off" in all four overrides worked, and cost more than it saved: - two of upstream's own coverage tests in check-max-lines-ratchet.test.mjs go red, because they assert the installed oxlint still flags an over-budget file; and - check-max-lines-ratchet.mjs becomes inert — the script whose whole job is to freeze who may bypass the rule cannot see a rule that is off. Measured with upstream's config restored, only NINE files exceeded budget: eight upstream-owned, over by 1, 3, 3, 9, 20, 23, 31 and 34 lines, and the fork's own local/sync/conflicts.mjs at 682. So the whole-repo cap was abandoned to avoid eight one-line exemptions. Each of the eight now carries `oxlint-disable max-lines -- Why:` naming what the fork adds, plus a config/max-lines-baseline.txt row — upstream's own escape hatch, whose message says to use it "with reviewer sign-off for an unavoidable exception". Splitting an upstream file to fit the budget is the eviction this whole change exists to stop: it costs a permanent hunk on every sync. conflicts.mjs is split rather than exempted, since it is ours: its rendering moved to local/sync/lib/conflict-report.mjs, leaving 568 lines. Behaviour verified by running the CLI, not by inspection. Verified: ratchet OK with 12 grandfathered entries (4 upstream's own), 19/19 in upstream's test file, 0 max-lines findings across the tree, .oxlintrc.json byte-identical to 0571e45107 again, 63 sync-lib tests and 4 fork-process-docs tests pass. LOCAL-PATCHES.md local(lint) and plan §5.2/§7 record the reversal as an addendum rather than rewriting the decision. Note: this commit also carries the config restore that the pre-commit hook swept into the previous commit's tree; they are one change and are committed as one. * feat(nightly): check the Mac allowlist's claims against the Linux runner local/unit-known-red.json says 17 test entries fail because of this Mac. Nothing ever checked that claim: a red Linux shard was read as "which shard", not as a verdict. local/check-nightly-linux-parity.mjs takes a nightly run's downloaded unit-shard artifacts and reports each allowlisted entry as passed, failed or absent on Linux. Absent is not a pass: a file never collected, a suite-level failure, or a test reported skipped all read as absent and exit non-zero. `prunedSince` is reported but not enforced, because those entries were pruned for passing locally, so their Linux outcome informs the next prune rather than gating it. No reports at all exits 2 -- a missing input is "cannot tell", never green. The pure core is local/lib/linux-parity.mjs, reusing allowlistEntries from unit-gate-judgement so the gate and this checker share one entry vocabulary. Measured, and it settles an assumption: against the 2026-10-26 run's five shard reports, ALL 17 allowlisted entries passed on Linux (22 of 23 prunedSince passed, 1 absent because a macOS-only bundle test is skipped there). So the Mac allowlist is Mac-specific as claimed, and Linux carries no environment-bound red set of its own -- which matches the 2026-10-27 dispatch, whose three red files were all commits missing from main rather than platform differences. Discoverability: when the liveness verdict names failed jobs it now points at this script, rather than asking a human to remember it exists. 14 fixture tests; empty-dir exit 2 and real-dir exit 0 both verified by hand. * perf(gate): one cold start for all isolation re-runs, and name the worker cap Two gate-cost items. Isolation re-runs were serial: each unexpected failing file got its own vitest cold start with a full 60-minute allowance, so N failures paid N cold starts. They now run as one invocation, with rerunReportsFor mapping the shared report back to each file by exact path, so attribution is unchanged and a file missing from the report still reads NO_RERUN (every failure reproduced -- fails closed). The 2,575 s run against a 1,400 s baseline is a worker cap, not more work. Reconstructed from the gate's own JSON reports: fast runs (10-23) showed up to 9 files concurrent and a 1,354-1,416 s suite; every run from 10-24 onward is capped at 2 concurrent, yet does LESS work -- 32,068 modules against 44,384, summed per-file time 2,209 s against 4,457 s, and faster per-file medians. So not CPU contention, not the mobile install, not the bun 1.4.4 pin (slow runs began before it reached this line). The cap of exactly 2 is VITEST_MAX_WORKERS, exported machine-wide from ~/.zshrc and ~/.zprofile (mtime 2026-10-23 17:02, between the last fast run at 15:40 and the first slow one). This commit only makes it visible: the gate now prints `gate-unit: VITEST_MAX_WORKERS=<n>` before spawning the suite, so the next slow run says why in one line instead of needing a per-file concurrency reconstruction. Whether to raise the cap is a machine decision, not a code one. The third item is closed as not-a-defect: recordDependencies=false is deliberate and documented, and with cold records check:changed stays selective rather than running everything. Tests: 3 new cases in tests/tools/gate-unit.test.mjs (33 pass), 24 pass in unit-gate.test.mjs. * docs(plan): §6.2 and §6.8 closed, with the numbers that closed them §6.2 is done and its assumption is now a measurement: against the 2026-10-26 run's five shard reports, all 17 Mac-allowlisted entries PASSED on Linux (22 of 23 prunedSince, one absent because a macOS-only bundle test is skipped there). So the Mac allowlist is Mac-specific as claimed and Linux carries no environment-bound red set of its own -- which the 10-27 dispatch confirms from the other direction, its three red files being commits missing from main rather than platform differences. The nightly is readable as a verdict now, not as "which shard". §6.8's doubling is a worker cap, measured: fast runs showed 9 files concurrent and 1,354-1,416 s; every run since 10-24 is capped at 2 and takes ~2,575 s while doing LESS work (32,068 modules vs 44,384, per-file medians 8.4 ms vs 13.4 ms). Not contention, not the mobile install, not the bun pin. The cap was VITEST_MAX_WORKERS from the shell profile; it is 4 now, and the gate prints the value so the next slow run answers this in one line. Isolation re-runs are batched (one cold start instead of N), and recordDependencies is closed as not-a-defect: the plan's premise that check:changed "runs everything" is wrong -- with cold records it stays selective. Also adds the §5.2 addendum: the budget is no longer a reason to evict upstream code, because an over-budget upstream file now gets an exemption rather than a restructure. * docs(lint): when a max-lines exemption is right, and a check for the half that is mechanical Adds the forward-looking rule to LOCAL-PATCHES.md § local(lint), because the exemption question will come up on every sync that grows an upstream file by a few lines. The rule is ownership, not size: - a file the fork owns and that is over budget gets SPLIT -- no exemption, ever. It has no upstream copy to conflict with, so the cap is a free guard there; - a file upstream owns gets one `oxlint-disable max-lines -- Why:` line plus a baseline row, however far over it is. Splitting it is a permanent hunk on every sync and drops upstream's later edits there silently; a two-line directive conflicts once, if ever. The cap exists to stop files growing without bound, not to push the fork into restructuring upstream's code -- which is why a 3-line overage was costing more than the rule saved. Also recorded: never raise the numbers in .oxlintrc.json (it weakens the guard for every file and desyncs the defaultLimitForPath constants that upstream's test pins), never turn the rule off (that is the reversal this section already records), and the probe recipe for listing offenders without editing the real config. tests/tools/max-lines-policy.test.mjs enforces the mechanical half: every baseline row must name a file that exists at the sync base, i.e. an upstream-owned one. Proven by adding `inline local/sync/conflicts.mjs` and watching it fail with the "must be split, not exempted" instruction. 3 cases pass on the real baseline. * refactor(pi): upstream's ownership fence beats the fork's, and the roster goes home Two restorations from §5.7, the first of them gated on evidence rather than judgement. The fork replaced upstream's omp session-owner module with a 204-line implementation (#68) that decides ownership by session file and refuses child sessions. Upstream's f37c1f6b95 fixes the same bug in 54 lines: ownership moves only on a session_start whose reason is new, resume or fork. So the gate was run before any deletion: the fork's #68 suite, unmodified, against upstream's rule -- 22/22 pass, including all twelve Pi-lane cases (in-process child, reopened child, re-key when a session_start names the leaving file, nested child, unpersisted session, the three handoffs, forked-session pane, refused transcript, persisted child off an unpersisted pane, switched unpersisted pane, no session manager). No case proved upstream's rule insufficient, so the 150-line difference bought nothing. The fork's module is deleted, upstream's file is live at f37c1f6b95's bytes (verified with an empty diff), and its own 11-case test passes. getOmpSessionOwnerHandlerSourceLines already takes `kind` upstream, so the handler source needed only its import and call site. Second: codex-subagent-roster.ts is back at upstream's path and name, byte-identical to the sync base, with five sibling files at zero fork hunks (codex-subagent-transcript.ts and test, providers/ codex-events.ts, providers/codex-transcript-poll.ts, codex-status-transcript-line.test.ts). The rename was costing a modify/delete plus importer rewrites on every sync for no behaviour. agent-descendant-roster.ts survives as the sibling carrying only the pi/claude lane's extras, 93 lines of codex-named duplicates dropped. Docs, because two of them had declared the rename correct: LOCAL-PATCHES' local(agents) entry and "present-but-dead" list are corrected, and the ledger's two entries get dated addenda that retire them for the codex half rather than rewriting what was decided. 478 deletions against 155 insertions. Verified: 22/22 (#68 suite) and 11/11 (upstream's), 5/5 roster test, tc:node clean, changed-code gate passed, 63 sync-lib and 4 fork-process-docs tests pass. * fix(renderer): the provider map tripped two of the repo's own linters The two lint steps the renderer agent could not run, both red on its new file: - audit:anti-slop rejects `pendingShape`: "shape" names structure, not a domain role. The field is the readout shown before a provider reports, so it is `pendingReadout` now, in the type, all three entries and its one consumer. - verify:localization-coverage flagged the three brand labels as unlocalized renderer strings. They are brand names shown verbatim, exactly like upstream's own `getProviderDisplayName` ('Claude', 'Codex', 'Gemini', 'OpenCode Go'), so translating them would be wrong: the reviewed exclusion goes in config/localization-coverage-allowlist.json, three entries, and the file's comment now says where the exclusion lives instead of claiming a never-translate policy nobody could find. gate-lint: 39 steps, was 37 green / 2 new red, now all green. tc:web clean; 42 pass in the visibility suite; changed-code gate passed. * fix(test): the event-loop arm's extra factor of two is not part of its claim The gate's one unexpected failure, twice: the foreign-sqlite event-loop test failed in a 137k-test suite at four workers, and passed 3/3 in isolation and under directory load. Its claim -- a worker-hosted open keeps the caller responsive while an inline one blocks it -- is a comparison between two arms measured in the same process, and the third assertion added a /2 margin on top of it. Under load that margin is scheduler delay on the worker arm, not the open: the arms differ in kind, but by less than a factor of two when four files run at once. So the assertion keeps the ordering claim and drops the margin. This is a two-line fork hunk in an upstream test file, deliberately, rather than an allowlist entry: an entry would mark a test as known red and then be reported prunable the next time it passed, which is churn on top of a real ambiguity. Why it appeared now: the worker cap went from 2 to 4, so the suite's own load doubled. That is the cost side of that change, measured once and fixed at the assertion rather than hidden. 1/1 passes; changed-code gate passed. * fix(review): a disabled provider was still reading OpenCode's credential store The Opus review of the branch found one blocker, and it was real: the rate-limits refactor moved the disabled gate into the injected resolvers, but OpenCode Go is the one provider whose fetcher treats an empty settings key as "look elsewhere" -- it resolves OpenCode's environment, its v2 credential database, its inline key and its auth file, and then can send the request. updateState nulling the result afterwards is too late, and it broke the promise in usage-provider-enablement.ts that a switched off provider reads no credential. The fix mirrors the pattern already in this file for cursor and grok: gate that one slot at the call site with disabledUsageProviderSnapshot(…
ELI5
Two runtime tests fail on a Mac and pass on Linux. This makes them pass on a Mac too, without changing what they test.
What Changed
Before: two tests in
repository-project-operations.spec.tsfailed on macOS:The runtime stores the folder's resolved path,
/private/var/folders/.... The tests compared it with the unresolved temp path,/var/folders/..., because macOS's temp dir sits behind a/privatesymlink.After: both tests resolve their temp dir with
realpathbefore using it, so they pass on macOS.Why
The runtime is right to store the resolved path; only the tests assumed there was no symlink.
realpathchanges nothing on Linux, so CI is unaffected. Only these two tests compare the stored path exactly, so no other test needs the change.Linked Issue
N/A (maintainer).
Visual Proof
N/A: test-only.
Testing
On macOS,
pnpm test src/main/runtime/orca-runtime.test.tsgave 2 failed / 1,503 passed before and 1,505 passed after. oxlint passes.Review
Agent skill upstream boundary
Notes
Test-only change, no product impact.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)