Repository navigation
fix(editor): a file from a server no longer reloads once after each restart - #31196
Conversation
…, so it no longer reloads after a restart
…path collision lookup
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to
|
…ed id instead of opening a second copy
…aved edits under its owned id
… the overview lookup a plain read
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
a7c64a7f-a9a8-41d4-a2b4-89798f08453b
📒 Files selected for processing: 11
config/renderer-layout-writer-baseline.txtsrc/renderer/src/components/editor/EditorConflictReviewSurface.tsxsrc/renderer/src/components/editor/editor-panel-draft-selector.test.tssrc/renderer/src/components/editor/editor-panel-draft-selector.tssrc/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.tssrc/renderer/src/components/editor/useEditorPanelContentState.tssrc/renderer/src/runtime/server-layout-frame-ids.test.tssrc/renderer/src/runtime/server-layout-frame-ids.tssrc/renderer/src/store/slices/editor-git-status-reconciliation.test.tssrc/renderer/src/store/slices/editor/actions/open-conflict-review.tssrc/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts
💤 Files with no reviewable changes: 1
- config/renderer-layout-writer-baseline.txt
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
| } | ||
|
|
||
| const absolutePath = joinPath(activeFile.filePath, entry.path) | ||
| if (needsFileRead(absolutePath)) { | ||
| void loadFileContent(absolutePath, absolutePath, activeFile.worktreeId, entry.path) | ||
| const fileId = conflictOverviewFileId(openFiles, activeFile.worktreeId, absolutePath) | ||
| if (needsFileRead(fileId)) { | ||
| void loadFileContent(absolutePath, fileId, activeFile.worktreeId, entry.path) | ||
| } | ||
| } | ||
| return |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,200p' src/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.ts
sed -n '125,205p' src/renderer/src/components/editor/useEditorPanelContentState.tsRepository: stablyai/orca
Length of output: 7368
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- conflictOverviewFileId definitions/usages ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'conflictOverviewFileId' -e 'resolveEditorFileIdForOwner' src/ || test "$?" -eq 1
printf '%s\n' '--- openFiles and conflict-review state references ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'setOpenFiles' -e 'openFiles:' -e 'selectedConflictReviewFile' -e "mode: 'conflict-review'" src/renderer/src/components/editor src/renderer/src/store src/renderer/src | head -n 240
printf '%s\n' '--- helper source ---'
helper=$(rg -l -F --glob '*.ts' --glob '*.tsx' 'export function conflictOverviewFileId' src/ | head -n 1)
if [ -n "$helper" ]; then
sed -n '1,240p' "$helper"
fi
printf '%s\n' '--- owner hook context ---'
sed -n '1,180p' src/renderer/src/components/editor/useEditorPanelContentState.ts
printf '%s\n' '--- relevant tests ---'
rg -n -F --glob '*.{test,spec}.ts' --glob '*.{test,spec}.tsx' -e 'conflictOverviewFileId' -e 'conflict-review' -e 'openFiles' src/renderer/src | head -n 240Repository: stablyai/orca
Length of output: 42134
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- editor-file-ids ---'
nl -ba src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts | sed -n '100,215p'
printf '%s\n' '--- open conflict review action ---'
nl -ba src/renderer/src/store/slices/editor/actions/open-conflict-review.ts | sed -n '1,180p'
printf '%s\n' '--- open conflict file action ---'
file=$(rg -l -F --glob '*.ts' 'resolveEditorFileIdForOwner(' src/renderer/src/store/slices/editor/actions | head -n 1)
printf 'candidate=%s\n' "$file"
if [ -n "$file" ]; then nl -ba "$file" | sed -n '1,150p'; fi
printf '%s\n' '--- content state derivation ---'
nl -ba src/renderer/src/components/editor/useEditorPanelContentState.ts | sed -n '1,210p'
printf '%s\n' '--- conflict surface lookup and callbacks ---'
nl -ba src/renderer/src/components/editor/EditorConflictReviewSurface.tsx | sed -n '1,125p'
nl -ba src/renderer/src/components/editor/EditorConflictReviewSurface.tsx | sed -n '230,280p'Repository: stablyai/orca
Length of output: 35999
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- open-file-apply ---'
nl -ba src/renderer/src/store/slices/editor/actions/open-file-apply.ts | sed -n '1,180p'
printf '%s\n' '--- workspace editor item ---'
nl -ba src/renderer/src/store/slices/editor/tabs/workspace-editor-item.ts | sed -n '1,240p'
printf '%s\n' '--- callers of openWorkspaceEditorItem and openFile actions ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'openWorkspaceEditorItem(' -e 'openFile(' -e 'openFileApply' src/renderer/src | head -n 260
printf '%s\n' '--- file content loader ---'
nl -ba src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts | sed -n '1,260p'Repository: stablyai/orca
Length of output: 43057
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- non-activating production open calls ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -g '!*.test.*' -g '!*.spec.*' -e "selection: 'none'" -e "selection: 'background'" -e 'openFile({' -e 'openFile(' src/renderer/src | head -n 320
printf '%s\n' '--- openFile action declaration and wrapper ---'
rg -n -F --glob '*.ts' -e 'openFile:' -e 'createOpenFile' -e 'applyOpenFileToState' src/renderer/src/store/slices/editor
printf '%s\n' '--- editor synchronization mutations ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -g '!*.test.*' -g '!*.spec.*' -e 'openFiles:' -e 'set((s)' -e 'setState({' src/renderer/src/runtime src/renderer/src/store | grep -E 'openFiles|migrat|mirror|sync|batch' | head -n 320
printf '%s\n' '--- migration implementation ---'
nl -ba src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts | sed -n '1,240p'Repository: stablyai/orca
Length of output: 18046
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- open-file action ---'
nl -ba src/renderer/src/store/slices/editor/actions/open-file-action.ts | sed -n '1,125p'
printf '%s\n' '--- OpenFileOptions and selection call sites ---'
nl -ba src/renderer/src/store/slices/editor/types/editor-files-slice.ts | sed -n '1,90p'
rg -n -C 4 --glob '*.ts' --glob '*.tsx' -g '!*.test.*' -g '!*.spec.*' -e 'selection:' -e 'openFile\(' src/renderer/src/components src/renderer/src/hooks src/renderer/src/runtime src/renderer/src/lib | head -n 360
printf '%s\n' '--- sync final patch ---'
nl -ba src/renderer/src/runtime/web-session-tabs-sync/apply-final-patch.ts | sed -n '1,180p'
printf '%s\n' '--- reparent implementation ---'
rg -n -F --glob '*.ts' -e 'reparentRestoredEditorFileOwner' src/renderer/srcRepository: stablyai/orca
Length of output: 41948
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- restored owner reparent ---'
nl -ba src/renderer/src/store/slices/editor/actions/restored-editor-owner.ts | sed -n '1,240p'
printf '%s\n' '--- nextOpenFiles producers ---'
rg -n -C 5 --glob '*.ts' -g '!*.test.*' -e 'nextOpenFiles' src/renderer/src/runtime src/renderer/src/store | head -n 360
printf '%s\n' '--- sync tab builders ---'
nl -ba src/renderer/src/runtime/web-session-tabs-sync/tab-builders.ts | sed -n '1,180p'Repository: stablyai/orca
Length of output: 30132
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- mirrored editor identity ---'
nl -ba src/renderer/src/runtime/web-session-tabs-sync/mirrored-editor-file-identity.ts | sed -n '1,260p'
printf '%s\n' '--- active-state reconciliation ---'
rg -n -C 6 --glob '*.ts' -g '!*.test.*' -e 'function applyActiveStateUpdates' -e 'nextActiveFileId' -e 'activeFileIdByWorktree' src/renderer/src/runtime/web-session-tabs-sync | head -n 320Repository: stablyai/orca
Length of output: 19590
Rerun the conflict overview load when openFiles changes.
The overview effect uses openFiles to select the content ID, but openFiles is not a dependency. If synchronization adds the matching server-owned edit tab while the review tab remains active, the overview switches from the path ID to the owned ID without loading content for that ID. The overview can remain stuck in its loading state.
Suggested fix
isVisible,
- gitStatusEntries
+ gitStatusEntries,
+ openFiles
])| let conflictFileId = absolutePath | ||
| set((s) => { | ||
| const conflict = toOpenConflictMetadata(entry) | ||
| const existing = s.openFiles.find((f) => f.id === absolutePath) | ||
| const runtimeEnvironmentId = conflictFileRuntimeOwner(s, worktreeId) | ||
| // Why: a server's file is open under its owned id; looking up the bare path would open a second copy. | ||
| const fileId = resolveEditorFileIdForOwner( | ||
| s, | ||
| absolutePath, | ||
| worktreeId, | ||
| runtimeEnvironmentId, | ||
| ['edit'] | ||
| ) | ||
| conflictFileId = fileId | ||
| const existing = s.openFiles.find((f) => f.id === fileId) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,115p' src/renderer/src/store/slices/editor/actions/open-conflict-review.ts
sed -n '105,205p' src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts
rg -n 'openConflictReviewFile|resolveEditorOperation.*Provenance|editorOperation.*Provenance' src/renderer/src/store/slices src/renderer/src/components/editor | head -100Repository: stablyai/orca
Length of output: 9149
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- provenance references ---'
rg -n -F -- 'captureEditorFileOperationProvenance' src shared || true
printf '%s\n' '--- provenance helper files ---'
rg -n -F -- 'export function captureEditorFileOperationProvenance' src shared || true
printf '%s\n' '--- conflict surface ---'
sed -n '1,130p' src/renderer/src/components/editor/EditorConflictReviewSurface.tsx
printf '%s\n' '--- conflict action remainder ---'
sed -n '100,260p' src/renderer/src/store/slices/editor/actions/open-conflict-review.ts
printf '%s\n' '--- conflict tests around callers ---'
sed -n '430,610p' src/renderer/src/store/slices/editor-git-status-reconciliation.test.tsRepository: stablyai/orca
Length of output: 20165
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- provenance helper ---'
sed -n '1,230p' src/renderer/src/lib/editor-file-operation-owner.ts
printf '%s\n' '--- provenance tests ---'
sed -n '1,220p' src/renderer/src/lib/editor-file-operation-owner.test.ts
printf '%s\n' '--- conflict reuse tests remainder ---'
sed -n '560,700p' src/renderer/src/store/slices/editor-git-status-reconciliation.test.ts
printf '%s\n' '--- editor owner tests around route behavior ---'
sed -n '1,210p' src/renderer/src/store/slices/editor-tab-host-ownership.test.tsRepository: stablyai/orca
Length of output: 25743
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- file-id owner predicates ---'
sed -n '1,125p' src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts
printf '%s\n' '--- route resolution references ---'
rg -n -F -e 'function resolveExplicitWorktreeOperationRouteResult' -e 'function resolveWorktreeOperationRoute' -e 'kind: '\''ambiguous'\''' src/renderer/src shared || true
printf '%s\n' '--- route implementation candidates ---'
sed -n '1,260p' src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts
printf '%s\n' '--- focused diff ---'
git diff 997d34b04389665795966ec4c0325b5cbfabf9ee d0d9374a06355a8e2f549105fe96c4ce64e9b56d -- src/renderer/src/store/slices/editor/actions/open-conflict-review.ts src/renderer/src/store/slices/editor/file-ids/editor-file-ids.tsRepository: stablyai/orca
Length of output: 23099
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- file-id owner predicates ---'
sed -n '1,125p' src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts
printf '%s\n' '--- route resolution references ---'
rg -n -F -e 'function resolveExplicitWorktreeOperationRouteResult' -e 'function resolveWorktreeOperationRoute' -e "kind: 'ambiguous'" src/renderer/src shared || true
printf '%s\n' '--- focused diff ---'
git diff 997d34b04389665795966ec4c0325b5cbfabf9ee d0d9374a06355a8e2f549105fe96c4ce64e9b56d -- src/renderer/src/store/slices/editor/actions/open-conflict-review.ts src/renderer/src/store/slices/editor/file-ids/editor-file-ids.tsRepository: stablyai/orca
Length of output: 15653
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- analogous open-file provenance handling ---'
sed -n '1,135p' src/renderer/src/store/slices/editor/actions/open-file-apply.ts
rg -n -F -e 'operationProvenance' -e 'captureEditorFileOperationProvenance' src/renderer/src/store/slices/editor/actions src/renderer/src/store/slices/editor | head -120
printf '%s\n' '--- catalog route implementation ---'
sed -n '1,110p' src/renderer/src/lib/worktree-operation-catalog-route.ts
printf '%s\n' '--- route ambiguity tests ---'
sed -n '50,95p' src/renderer/src/lib/worktree-operation-route.test.ts
sed -n '210,250p' src/renderer/src/lib/worktree-operation-route.test.ts
sed -n '600,640p' src/renderer/src/lib/worktree-operation-route.test.ts
sed -n '690,715p' src/renderer/src/lib/worktree-operation-route.test.tsRepository: stablyai/orca
Length of output: 23720
Do not treat an unresolved owner as local.
When captureEditorFileOperationProvenance cannot resolve the worktree, conflictFileRuntimeOwner returns null. resolveEditorFileIdForOwner then does not match an existing server-owned file and can return the bare path. The action appends a second OpenFile for that path.
Keep the unresolved state distinct from a valid local owner and stop the conflict-file open until the owner resolves.
Suggested fix
-function conflictFileRuntimeOwner(s: AppState, worktreeId: string): string | null {
+function conflictFileRuntimeOwner(
+ s: AppState,
+ worktreeId: string
+): string | null | undefined {
try {
return captureEditorFileOperationProvenance(s, worktreeId, undefined, false).generation.route
.runtimeEnvironmentId
} catch {
- return null
+ return undefined
}
}
...
const conflict = toOpenConflictMetadata(entry)
const runtimeEnvironmentId = conflictFileRuntimeOwner(s, worktreeId)
+ if (runtimeEnvironmentId === undefined) {
+ openedConflictFile = false
+ return s
+ }
// Why: a server's file is open under its owned id; looking up the bare path would open a second copy.There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Make conflictOverviewFileId owner-aware. · editor-file-ids.ts:187-197
src/renderer/src/store/slices/editor/file-ids/editor-file-ids.ts:187-197
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake
conflictOverviewFileIdowner-aware.Before an entry is selected, the conflict-review tab renders its overview. The overview, loader, and draft selector all use
conflictOverviewFileId, which currently returns the first edit tab matching only worktree and path, or the raw path. If a local edit tab precedes the server-owned edit tab, the overview can read the local buffer, content, or draft instead of the server-owned file.Apply the same owner-aware resolution used for
selectedFileIdinsideconflictOverviewFileId, using the review's resolved runtime owner. Keep all overview lookups routed through this shared helper.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
12029c90-9377-48c9-a8bc-017303e52fed
📒 Files selected for processing: 1
src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.
…ping-check tests After #31196 a server's tab is no longer keyed by its path, so these tests flagged a tab that does not exist and no read was ever issued. Co-Authored-By: Claude <noreply@anthropic.com>
… test-only editor-state guard (#31250) * refactor(editor): drop isWholeEditorState and the diff-signature re-export; bound superseded restore re-reads - Remove isWholeEditorState; its callers take the real AppState. Tests that mocked partial stores now build full states from createTestStore. - Import getDiskBaselineSignature from shared/disk-baseline-signature directly and delete the re-export from diff-content-signature.ts. - Restored-tab conflict scan: count superseded re-reads toward MAX_VERIFY_ATTEMPTS, so a read slower than the re-verify interval gives up with the conflict banner instead of looping forever. Adds a regression test. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): keep the last read at the superseded re-read cap instead of giving up A matching read at the cap cleared nothing and showed a false changed-on-disk banner; it now resolves like any read. Only read failures give up. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): back off on superseded restore re-reads instead of capping them A superseded read may predate the newer check, so it never resolves the verification, even at the attempt cap. Overlapping checks now delay the next fresh read (bounded, one timer per tab) and keep verification pending; only failed reads count toward giving up. Co-Authored-By: Claude <noreply@anthropic.com> * test(editor): address the restored tab by its owned id in the overlapping-check tests After #31196 a server's tab is no longer keyed by its path, so these tests flagged a tab that does not exist and no read was ever issued. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
#31196 edited the removed renderer Markdown bridge and moved the editor save queue's precondition check; the bridge stays deleted and the queue keeps main's order without the 'mobile' trigger.
ELI5
When you open a file that lives on a paired server, Orca gives it an internal name. It used to give the file one name when you first opened it and a different name when Orca restarted. Because the name changed, the editor reloaded that file once after every restart. Now the file gets its final name the first time you open it, so a restart has nothing to rename and the file does not reload.
What Changed
Before: a file from a server reloads once after each restart.
After: it keeps its identity across restarts, and there is no reload.
How it works:
hydrated-editor-session-state.ts). First open did not: it used the bare path unless another owner already held that path. So at the first restart the id changed, and the editor tab rebuilt itself. Unsaved drafts were carried over, but the editor still reloaded.resolveEditorFileIdForOwner(editor-file-ids.ts) now returns the owned id for any server file the first time it is opened. Files on this computer still use their path. The desktop's live mirror of a server's tabs uses the same naming (mirrored-editor-file-identity.ts). Because a server file is now always given an owned id, the mirror's lookup for a bare-path collision could never matter any more, so it was removed.server-layout-frame-ids.ts). That way a workspace drawn from frames and the same workspace drawn by the mirror never disagree on a file's id. That code runs only with the switch on. Its reverse direction has no caller yet, and its comment now says that a tab the server named by path comes back under the server's owned id.Owner decision Q15. Owner-visible row 24: "server file no longer reloads once after restart". This change applies to both builds and is not behind the layout switch.
Sweep: code that might assume a server file's id equals its path
Key point: every id this change produces already appears today after any restart. So the sweep looked for code that only worked during the first session.
open-file-apply.ts:72-89mirrored-editor-file-identity.ts:57-67,tab-builders.ts:118-150hydrated-editor-session-state.ts:66-84,hydrated-editor-file-ids.ts:53-63workspace-session.ts:137,editorDrafts[id]everywhereisEditorFileIdOccupiedByOtherOwner(editor-file-ids.ts:105-118)remap-open-editor-tabs-for-path-change.ts:241-266markdown-preview-actions.ts:106-114,EditorContent.tsx:216,EditorPanel.tsx:300,editor-panel-render-model.ts:169?? filePathfallbacks only apply to old records that lack a source id.getOpenedEditFileIdAfterOpen(editor-file-ids.ts:160-180),markdown-preview-source-routing.ts:41-64search-match-open.ts:52-76,check-annotation-open.ts:35-55,use-note-opening.ts:74-90,terminal-file-open-routing.tsopenFile. The line reveal matches by path when no id is given (EditorEditFileSurface.tsx:327).FileExplorerVirtualRows.tsx:236(activeFileId === n.path)useFileExplorerAutoReveal.ts:70-85).mobile-markdown-bridge.ts:177-195,mobile-session-tab-close.ts:14buildDiffEditorFileId,open-history-diff.tsopen-conflict-review.tsopenFiledoes and uses the same owner-aware id. If the owner cannot be worked out, it falls back to the old path lookup rather than guessing. A server file it opens itself carries its owner, so a later open reuses it. A local file is unchanged.EditorConflictReviewSurface.tsx,useEditorPanelActiveTabContentLoad.ts,editor-panel-draft-selector.tsworkspace-layout-editor-tab-file.ts:10-16,orcad-source-left-behind-session-rows.ts:151,profile-session-markdown-transfer.ts:79local-structured-session-tabs-sync)Why
The other fix would have been to make restore keep the bare path for a server file. That would bring back two owners colliding on one id: a local file and a server file with the same path, or the same server path in two workspaces. The owned id was introduced to prevent exactly that. Naming the file correctly at the single place ids are minted (
resolveEditorFileIdForOwner, used by open, the live mirror and the rename remap) makes the first open match what restore already produces, and adds no new rule.Linked Issue
Owner decision Q15 (no tracker issue).
Visual Proof
N/A. The visible symptom is a one-time editor reload just after a restart with a paired server. The new tests check the cause directly: the file id and the tab ids are unchanged across a save-and-restore round trip. I did not run the app with a paired server.
Testing
New
server-editor-file-restart-identity.test.ts:On base, tests 1 to 4 fail and test 5 passes, as intended.
Conflict review (
editor-git-status-reconciliation.test.ts): (a) a server file is already open and then picked in conflict review: the existing copy is reused, with no duplicate, and conflict review selects it. (b) conflict review opens a server file first, and a later normal open reuses it. (c) a local file is unchanged. Before the conflict-review fix, (a) and (b) fail. Overview drafts (editor-panel-draft-selector.test.ts): an open server file's unsaved edits show in the overview. This fails before the overview fix.Server layout frames: a path held by two owners on the server still maps to the one owned id for that path, whichever record is matched. That id depends only on the path, so the code now checks only the path. Existing tests that opened a server file and then used its path as its id now use the id
openFilereturns, or the owned id the mirror gives. No assertion was dropped. Local runs: 82 directly affected test files pass (911 tests), plus 136 more files that touch server-owned editor files (1509 tests), plus every test file that touches conflicts or the editor panel (172 files, 2148 tests). The CI static gate (ci-static-gate.mjs) passed at this head. Nosrc/mainchanges.Review
An independent review found no blocking bugs. It raised three points, all handled: conflict review is now fixed in this PR (see the sweep table), the reverse frame mapping's comment is corrected, and the now-unused collision lookup in the mirror was removed. Each later change also had its own review, and none found blocking bugs.
Agent skill upstream boundary
Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)