Merge remote-tracking branch 'origin/main' into brennanb2025/mobile-launch-points-agent-launch

# Conflicts:
#	mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json
#	mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json
#	mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json
#	mobile/rpc-foundation/goldens/aivault-history-screen-listed.json
#	mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json
#	mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json
#	mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json
#	mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json
#	mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json
#	mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json
#	mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json
#	mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json
#	mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json
#	mobile/rpc-foundation/goldens/b1.json
#	mobile/rpc-foundation/goldens/b2.json
#	mobile/rpc-foundation/goldens/b3.json
#	mobile/rpc-foundation/goldens/browser-dialog-accepted.json
#	mobile/rpc-foundation/goldens/browser-dialog-dismissed.json
#	mobile/rpc-foundation/goldens/browser-keyboard-input.json
#	mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json
#	mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json
#	mobile/rpc-foundation/goldens/browser-wheel-scrolled.json
#	mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json
#	mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json
#	mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json
#	mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json
#	mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json
#	mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json
#	mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json
#	mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json
#	mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json
#	mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json
#	mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json
#	mobile/rpc-foundation/goldens/components-codex-capability.json
#	mobile/rpc-foundation/goldens/components-setup-ask.json
#	mobile/rpc-foundation/goldens/components-target-local.json
#	mobile/rpc-foundation/goldens/components-target-ssh.json
#	mobile/rpc-foundation/goldens/diff-review-branch-compare.json
#	mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json
#	mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json
#	mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json
#	mobile/rpc-foundation/goldens/diff-review-snapshot.json
#	mobile/rpc-foundation/goldens/diff-review-status-unavailable.json
#	mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json
#	mobile/rpc-foundation/goldens/file-tap-open-refused.json
#	mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json
#	mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json
#	mobile/rpc-foundation/goldens/file-tap-resolve-miss.json
#	mobile/rpc-foundation/goldens/file-tap-resolve-refused.json
#	mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json
#	mobile/rpc-foundation/goldens/files-explorer-readdir.json
#	mobile/rpc-foundation/goldens/files-ownership-local.json
#	mobile/rpc-foundation/goldens/files-ownership-ssh.json
#	mobile/rpc-foundation/goldens/files-preview-artifact-direct.json
#	mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json
#	mobile/rpc-foundation/goldens/files-preview-artifact-image.json
#	mobile/rpc-foundation/goldens/files-preview-grant-refresh.json
#	mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json
#	mobile/rpc-foundation/goldens/files-preview-worktree-image.json
#	mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json
#	mobile/rpc-foundation/goldens/files-preview-worktree.json
#	mobile/rpc-foundation/goldens/files-save-blind.json
#	mobile/rpc-foundation/goldens/files-save-verified.json
#	mobile/rpc-foundation/goldens/files-tab-doc-shapes.json
#	mobile/rpc-foundation/goldens/home-host-accounts.json
#	mobile/rpc-foundation/goldens/home-host-stats.json
#	mobile/rpc-foundation/goldens/host-view-settings-sync.json
#	mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json
#	mobile/rpc-foundation/goldens/host-worktree-delete-refused.json
#	mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json
#	mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json
#	mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json
#	mobile/rpc-foundation/goldens/inventory-lifecycle.json
#	mobile/rpc-foundation/goldens/inventory-repeat-query.json
#	mobile/rpc-foundation/goldens/lifecycle-b3.json
#	mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json
#	mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json
#	mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json
#	mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json
#	mobile/rpc-foundation/goldens/linear-select-workspace.json
#	mobile/rpc-foundation/goldens/live-worktree-name-stream.json
#	mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json
#	mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json
#	mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json
#	mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json
#	mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json
#	mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json
#	mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json
#	mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json
#	mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json
#	mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json
#	mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json
#	mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json
#	mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json
#	mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json
#	mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json
#	mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json
#	mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json
#	mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json
#	mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-artifact-image-files.readterminalartifactpreview-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-worktree-image-files.readpreview-1.json
#	mobile/rpc-foundation/goldens/matrix-files.preview-worktree-text-files.read-1.json
#	mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json
#	mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json
#	mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json
#	mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json
#	mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json
#	mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json
#	mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json
#	mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json
#	mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json
#	mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json
#	mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json
#	mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json
#	mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json
#	mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json
#	mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json
#	mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json
#	mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json
#	mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json
#	mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json
#	mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json
#	mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json
#	mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json
#	mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json
#	mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json
#	mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json
#	mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json
#	mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json
#	mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json
#	mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json
#	mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json
#	mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json
#	mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json
#	mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json
#	mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json
#	mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json
#	mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json
#	mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json
#	mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json
#	mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json
#	mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1.json
#	mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json
#	mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json
#	mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json
#	mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json
#	mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json
#	mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json
#	mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json
#	mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json
#	mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json
#	mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json
#	mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json
#	mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json
#	mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json
#	mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json
#	mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json
#	mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json
#	mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json
#	mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json
#	mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json
#	mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-files.read-1.json
#	mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-markdown.readtab-1.json
#	mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json
#	mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json
#	mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json
#	mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json
#	mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json
#	mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json
#	mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json
#	mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json
#	mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json
#	mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json
#	mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json
#	mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json
#	mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json
#	mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json
#	mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json
#	mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json
#	mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json
#	mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json
#	mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json
#	mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json
#	mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json
#	mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json
#	mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json
#	mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json
#	mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json
#	mobile/rpc-foundation/goldens/native-chat-image-paste-single.json
#	mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json
#	mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json
#	mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json
#	mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json
#	mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json
#	mobile/rpc-foundation/goldens/native-chat-image-upload-single.json
#	mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json
#	mobile/rpc-foundation/goldens/native-chat-image-upload-two.json
#	mobile/rpc-foundation/goldens/native-chat-page-earlier.json
#	mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json
#	mobile/rpc-foundation/goldens/native-chat-readability-refused.json
#	mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json
#	mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json
#	mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json
#	mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json
#	mobile/rpc-foundation/goldens/native-chat-stop-accepted.json
#	mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json
#	mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json
#	mobile/rpc-foundation/goldens/native-chat-write-accepted.json
#	mobile/rpc-foundation/goldens/native-chat-write-clear-line.json
#	mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json
#	mobile/rpc-foundation/goldens/native-chat-write-rejected.json
#	mobile/rpc-foundation/goldens/native-chat-write-typed-command.json
#	mobile/rpc-foundation/goldens/new-tab-local-agents.json
#	mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json
#	mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json
#	mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json
#	mobile/rpc-foundation/goldens/notifications-desktop-stream.json
#	mobile/rpc-foundation/goldens/notifications-display-test-accepted.json
#	mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json
#	mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json
#	mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json
#	mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json
#	mobile/rpc-foundation/goldens/notifications-push-registered.json
#	mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json
#	mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json
#	mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json
#	mobile/rpc-foundation/goldens/pr-branch-identity.json
#	mobile/rpc-foundation/goldens/pr-branch-repo-context.json
#	mobile/rpc-foundation/goldens/pr-comment-mutation.json
#	mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json
#	mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json
#	mobile/rpc-foundation/goldens/pr-mutation-status.json
#	mobile/rpc-foundation/goldens/pr-read-fork-routing.json
#	mobile/rpc-foundation/goldens/pr-read-surface.json
#	mobile/rpc-foundation/goldens/pr-read-upstream-error.json
#	mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json
#	mobile/rpc-foundation/goldens/pr-sidebar-load.json
#	mobile/rpc-foundation/goldens/pr-title-mutation.json
#	mobile/rpc-foundation/goldens/pr-title-unconfirmed.json
#	mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json
#	mobile/rpc-foundation/goldens/pr-triage-launch.json
#	mobile/rpc-foundation/goldens/pr-triage-send-locked.json
#	mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json
#	mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json
#	mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json
#	mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json
#	mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json
#	mobile/rpc-foundation/goldens/quick-commands-load-refused.json
#	mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json
#	mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json
#	mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json
#	mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json
#	mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json
#	mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json
#	mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json
#	mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json
#	mobile/rpc-foundation/goldens/review-branch-diff-shapes.json
#	mobile/rpc-foundation/goldens/review-create-terminal-refused.json
#	mobile/rpc-foundation/goldens/review-file-diff-shapes.json
#	mobile/rpc-foundation/goldens/review-git-mutations-run.json
#	mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json
#	mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json
#	mobile/rpc-foundation/goldens/review-open-in-session.json
#	mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json
#	mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json
#	mobile/rpc-foundation/goldens/review-stage-file.json
#	mobile/rpc-foundation/goldens/review-stage-refused.json
#	mobile/rpc-foundation/goldens/sc-base-ref-default.json
#	mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json
#	mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json
#	mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json
#	mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json
#	mobile/rpc-foundation/goldens/sc-changes-loaded.json
#	mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json
#	mobile/rpc-foundation/goldens/sc-commit-message-canceled.json
#	mobile/rpc-foundation/goldens/sc-commit-message-generated.json
#	mobile/rpc-foundation/goldens/sc-create-existing-review.json
#	mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json
#	mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json
#	mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json
#	mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json
#	mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json
#	mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json
#	mobile/rpc-foundation/goldens/sc-eligibility-fetched.json
#	mobile/rpc-foundation/goldens/sc-history-commit-files.json
#	mobile/rpc-foundation/goldens/sc-history-loaded.json
#	mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json
#	mobile/rpc-foundation/goldens/sc-pr-link-read.json
#	mobile/rpc-foundation/goldens/sc-pr-link-set.json
#	mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json
#	mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json
#	mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json
#	mobile/rpc-foundation/goldens/sc-prerequisite-publish.json
#	mobile/rpc-foundation/goldens/sc-prerequisite-push.json
#	mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json
#	mobile/rpc-foundation/goldens/sc-reveal-first-poll.json
#	mobile/rpc-foundation/goldens/sc-reveal-timeout.json
#	mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json
#	mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json
#	mobile/rpc-foundation/goldens/sc-review-commit-rejected.json
#	mobile/rpc-foundation/goldens/sc-review-commit.json
#	mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json
#	mobile/rpc-foundation/goldens/sc-review-status-normalized.json
#	mobile/rpc-foundation/goldens/schedules-b3.json
#	mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json
#	mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json
#	mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json
#	mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json
#	mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json
#	mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json
#	mobile/rpc-foundation/goldens/session-browser-tab-created.json
#	mobile/rpc-foundation/goldens/session-create-browser-refused.json
#	mobile/rpc-foundation/goldens/session-create-browser-tab.json
#	mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json
#	mobile/rpc-foundation/goldens/session-create-markdown-note.json
#	mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json
#	mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json
#	mobile/rpc-foundation/goldens/session-create-terminal-refused.json
#	mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json
#	mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json
#	mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json
#	mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json
#	mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json
#	mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json
#	mobile/rpc-foundation/goldens/session-diff-notes-loaded.json
#	mobile/rpc-foundation/goldens/session-file-tab-read.json
#	mobile/rpc-foundation/goldens/session-markdown-disk-read.json
#	mobile/rpc-foundation/goldens/session-markdown-disk-served.json
#	mobile/rpc-foundation/goldens/session-markdown-save-conflict.json
#	mobile/rpc-foundation/goldens/session-markdown-saved.json
#	mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json
#	mobile/rpc-foundation/goldens/session-markdown-tab-read.json
#	mobile/rpc-foundation/goldens/session-markdown-tab-refused.json
#	mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json
#	mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json
#	mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json
#	mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json
#	mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json
#	mobile/rpc-foundation/goldens/session-tab-activation-refused.json
#	mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json
#	mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json
#	mobile/rpc-foundation/goldens/session-tab-close-session-tab.json
#	mobile/rpc-foundation/goldens/session-tab-close-terminal.json
#	mobile/rpc-foundation/goldens/session-tab-closed.json
#	mobile/rpc-foundation/goldens/session-tab-rename.json
#	mobile/rpc-foundation/goldens/session-tab-renamed.json
#	mobile/rpc-foundation/goldens/session-tabs-health-errored.json
#	mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json
#	mobile/rpc-foundation/goldens/session-tabs-health-refused.json
#	mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json
#	mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json
#	mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json
#	mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json
#	mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json
#	mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json
#	mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json
#	mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json
#	mobile/rpc-foundation/goldens/session-terminal-list-merged.json
#	mobile/rpc-foundation/goldens/session-terminal-list-refused.json
#	mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json
#	mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json
#	mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json
#	mobile/rpc-foundation/goldens/settings-home-coalesced.json
#	mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json
#	mobile/rpc-foundation/goldens/settings-home-providers-refused.json
#	mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json
#	mobile/rpc-foundation/goldens/settings-new-tab-refused.json
#	mobile/rpc-foundation/goldens/settings-new-tab-ssh.json
#	mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json
#	mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json
#	mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json
#	mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json
#	mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json
#	mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json
#	mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json
#	mobile/rpc-foundation/goldens/settings-task-hydration-refused.json
#	mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json
#	mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json
#	mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json
#	mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-task-workspace-refused.json
#	mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json
#	mobile/rpc-foundation/goldens/settings-task-write.json
#	mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json
#	mobile/rpc-foundation/goldens/settings-workspace-context-refused.json
#	mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json
#	mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json
#	mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json
#	mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json
#	mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json
#	mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json
#	mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json
#	mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json
#	mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json
#	mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json
#	mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json
#	mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json
#	mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json
#	mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json
#	mobile/rpc-foundation/goldens/structured-agent-session-created.json
#	mobile/rpc-foundation/goldens/structured-launch-created.json
#	mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json
#	mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json
#	mobile/rpc-foundation/goldens/structured-launch-support-refused.json
#	mobile/rpc-foundation/goldens/structured-launch-unsupported.json
#	mobile/rpc-foundation/goldens/tasks-route-repo-list.json
#	mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json
#	mobile/rpc-foundation/goldens/terminal-input-send-accepted.json
#	mobile/rpc-foundation/goldens/terminal-input-send-refused.json
#	mobile/rpc-foundation/goldens/terminal-live-input-accepted.json
#	mobile/rpc-foundation/goldens/terminal-paste-accepted.json
#	mobile/rpc-foundation/goldens/terminal-paste-refused.json
#	mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json
#	mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json
#	mobile/rpc-foundation/goldens/terminal-raw-input-refused.json
#	mobile/rpc-foundation/goldens/terminal-raw-input-reported.json
#	mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json
#	mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json
#	mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json
#	mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json
#	mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json
#	mobile/rpc-foundation/goldens/tk-create-github.json
#	mobile/rpc-foundation/goldens/tk-create-gitlab.json
#	mobile/rpc-foundation/goldens/tk-create-linear.json
#	mobile/rpc-foundation/goldens/tk-item-checks-files.json
#	mobile/rpc-foundation/goldens/tk-item-comment-github.json
#	mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json
#	mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json
#	mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json
#	mobile/rpc-foundation/goldens/tk-item-detail-github.json
#	mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json
#	mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json
#	mobile/rpc-foundation/goldens/tk-item-detail-linear.json
#	mobile/rpc-foundation/goldens/tk-item-detail-metadata.json
#	mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json
#	mobile/rpc-foundation/goldens/tk-item-metadata-github.json
#	mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json
#	mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json
#	mobile/rpc-foundation/goldens/tk-item-reply-merge.json
#	mobile/rpc-foundation/goldens/tk-item-review-github.json
#	mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json
#	mobile/rpc-foundation/goldens/tk-item-status-gitlab.json
#	mobile/rpc-foundation/goldens/tk-linear-connect.json
#	mobile/rpc-foundation/goldens/tk-linear-item.json
#	mobile/rpc-foundation/goldens/tk-linear-team-context.json
#	mobile/rpc-foundation/goldens/tk-list-gitlab-items.json
#	mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json
#	mobile/rpc-foundation/goldens/tk-list-linear.json
#	mobile/rpc-foundation/goldens/tk-project-board-load.json
#	mobile/rpc-foundation/goldens/tk-project-repo-slugs.json
#	mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json
#	mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json
#	mobile/rpc-foundation/goldens/tk-project-row-detail.json
#	mobile/rpc-foundation/goldens/tk-project-row-fields.json
#	mobile/rpc-foundation/goldens/tk-project-row-files-merge.json
#	mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json
#	mobile/rpc-foundation/goldens/tk-project-row-review-checks.json
#	mobile/rpc-foundation/goldens/tk-project-row-threads.json
#	mobile/rpc-foundation/goldens/tk-provider-load.json
#	mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json
#	mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json
#	mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json
#	mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json
#	mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json
#	mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json
#	mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json
#	mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json
#	mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json
#	mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json
#	mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json
#	mobile/rpc-foundation/goldens/tw-capabilities-advertised.json
#	mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json
#	mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json
#	mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json
#	mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json
#	mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json
#	mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json
#	mobile/rpc-foundation/goldens/tw-create-retry-created.json
#	mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json
#	mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json
#	mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json
#	mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json
#	mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json
#	mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json
#	mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json
#	mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json
#	mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json
#	mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json
#	mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json
#	mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json
#	mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json
#	mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json
#	mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json
#	mobile/rpc-foundation/goldens/tw-workspace-source-presets.json
#	mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json
#	mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json
#	mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json
#	mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json
#	mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json
#	mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json
#	mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json
#	mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json
#	mobile/rpc-foundation/goldens/worktree-home-catalog.json
#	mobile/rpc-foundation/goldens/worktree-retired-names.json
#	mobile/rpc-foundation/pilot-scenarios.json
This commit is contained in:
Brennan Benson
2026-09-28 12:54:45 -07:00
2089 changed files with 46906 additions and 14177 deletions
@@ -2,6 +2,10 @@ name: Install Node dependencies
description: Installs the Node toolchain and repository dependencies for CI jobs, with optional Electron archive caching.
inputs:
cache-pnpm-verification:
description: Restore pnpm's policy-checked lockfile verification record on Linux.
required: false
default: 'true'
native-runtime:
description: Native runtime to prepare after the script-free install (none, node, or electron).
required: false
@@ -24,6 +28,15 @@ inputs:
default: 'false'
outputs:
verification-cache-hit:
description: Whether pnpm's verification record was restored.
value: ${{ steps.verification-cache-restore.outputs.cache-hit }}
verification-cache-path:
description: The small pnpm-owned verification record, without registry metadata.
value: ${{ steps.verification-cache.outputs.path }}
verification-cache-key:
description: Exact verification record key, also used by the isolated PR benchmark.
value: ${{ steps.verification-cache.outputs.key }}
node-version:
description: Resolved Node.js version used for the install.
value: ${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}
@@ -86,6 +99,25 @@ runs:
path: ${{ steps.pnpm-store.outputs.path }}
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
- name: Resolve pnpm verification cache
id: verification-cache
if: runner.os == 'Linux' && inputs.cache-pnpm-verification == 'true'
shell: bash
env:
POLICY_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc') }}
run: |
printf 'path=%s/lockfile-verified.jsonl\n' "$(pnpm cache path)" >> "$GITHUB_OUTPUT"
printf 'key=pnpm-verification-v1-%s-%s-%s-%s\n' "$RUNNER_OS" "$RUNNER_ARCH" "$(pnpm --version)" "$POLICY_HASH" >> "$GITHUB_OUTPUT"
- name: Restore pnpm verification record
id: verification-cache-restore
if: steps.verification-cache.outputs.key != ''
continue-on-error: true
uses: actions/cache/restore@v5
with:
path: ${{ steps.verification-cache.outputs.path }}
key: ${{ steps.verification-cache.outputs.key }}
- name: Validate native runtime
shell: bash
env:
@@ -120,6 +152,15 @@ runs:
git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
fi
# pnpm checks the cached record's policy and validity; never bypass verification.
- name: Save pnpm verification record on main
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache-restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: ${{ steps.verification-cache.outputs.path }}
key: ${{ steps.verification-cache.outputs.key }}
- name: Resolve Electron package cache
id: electron-package-cache
if: inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'
@@ -140,12 +181,19 @@ runs:
printf 'version=%s\n' "$(node -p "require('./node_modules/electron/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Electron package archive
if: inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'
if: (github.event_name != 'pull_request' || runner.os != 'Linux') && steps.electron-package-cache.outputs.version != ''
uses: actions/cache@v5
with:
path: ${{ steps.electron-package-cache.outputs.cache-root }}
key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }}
- name: Restore Electron package archive without saving
if: github.event_name == 'pull_request' && runner.os == 'Linux' && steps.electron-package-cache.outputs.version != ''
uses: actions/cache/restore@v5
with:
path: ${{ steps.electron-package-cache.outputs.cache-root }}
key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }}
# Why cached: `--ignore-scripts` leaves node-pty without build/Release, so
# ensure-native-runtime node-gyp-compiles it in every job that asks for a runtime.
# The artifacts are ABI-bound, so the key carries the target runtime, the resolved
@@ -0,0 +1,33 @@
name: Restore xterm build dependencies
description: Reuse installed dependencies for the pinned upstream checkout.
outputs:
cache-key:
description: Exact dependency cache key.
value: ${{ steps.restore.outputs.cache-primary-key }}
cache-hit:
description: Whether the exact installed dependency cache was restored.
value: ${{ steps.restore.outputs.cache-hit }}
runs:
using: composite
steps:
- id: runtime
shell: bash
run: |
. /etc/os-release
echo "image=$ID-$VERSION_ID" >> "$GITHUB_OUTPUT"
echo "node=$(node --version)" >> "$GITHUB_OUTPUT"
- id: restore
uses: actions/cache/restore@v5
with:
path: |
${{ runner.temp }}/xterm-patch-build/upstream/.git
${{ runner.temp }}/xterm-patch-build/upstream/node_modules
key: xterm-dependencies-v1-${{ runner.os }}-${{ steps.runtime.outputs.image }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-${{ hashFiles('config/patches/xterm-upstream.json', 'config/scripts/regenerate-xterm-patches.mjs', 'config/scripts/xterm-patch-text.mjs', '.github/actions/prepare-xterm-dependencies/action.yml') }}
- name: Restore download cache on an installed-dependency miss
if: steps.restore.outputs.cache-hit != 'true'
uses: actions/cache/restore@v5
with:
path: |
~/.npm
${{ runner.temp }}/xterm-patch-build/upstream/.git
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
+18 -7
View File
@@ -2,6 +2,7 @@ name: Bun profile persistence
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'src/**'
- 'config/**'
@@ -17,6 +18,8 @@ on:
- '.github/actions/install-node-dependencies/**'
- '.github/workflows/bun-profile-tests.yml'
workflow_dispatch:
schedule:
- cron: '30 11 * * *'
permissions:
contents: read
@@ -32,6 +35,8 @@ jobs:
timeout-minutes: 5
outputs:
should_run: ${{ steps.scope.outputs.should_run }}
qualification: ${{ steps.scope.outputs.qualification }}
runners: ${{ steps.scope.outputs.runners }}
steps:
- uses: actions/checkout@v6
with:
@@ -56,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-2022, windows-11-arm]
os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-14","macos-15-intel","windows-2022","windows-11-arm"]') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
@@ -82,9 +87,12 @@ jobs:
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018
linux_glibc_floor:
needs: changes
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
needs: [changes, persistence]
# A failed smoke already blocks qualification; missing scope still selects every platform.
if: >-
${{ !cancelled() && needs.persistence.result == 'success' &&
needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' }}
strategy:
fail-fast: false
matrix:
@@ -107,9 +115,12 @@ jobs:
- run: pnpm test:bun:profile --artifact
linux_musl:
needs: changes
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
needs: [changes, persistence]
# A failed smoke already blocks qualification; missing scope still selects every platform.
if: >-
${{ !cancelled() && needs.persistence.result == 'success' &&
needs.changes.outputs.should_run != 'false' &&
needs.changes.outputs.qualification != 'false' }}
strategy:
fail-fast: false
matrix:
+7
View File
@@ -54,6 +54,10 @@ jobs:
with:
native-runtime: node
node-version: '24'
cache-electron-package: 'true'
- name: Populate shared Electron archive
run: node config/scripts/install-electron-package-binary.mjs
- uses: ./.github/actions/prepare-git-compatibility
@@ -83,6 +87,9 @@ jobs:
with:
native-runtime: node
node-version: '24'
cache-electron-package: 'true'
- name: Populate shared Electron archive
run: node config/scripts/install-electron-package-binary.mjs
- name: Verify native cache is usable
run: node config/scripts/ensure-native-runtime.mjs --check-only
+33
View File
@@ -0,0 +1,33 @@
name: Clean closed PR caches
on:
pull_request_target:
types: [closed]
permissions:
actions: write
jobs:
clean:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# No checkout: this runs trusted default-branch code, including for fork PRs.
- uses: actions/github-script@v8
with:
script: |
const ref = `refs/pull/${context.payload.pull_request.number}/merge`
const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
...context.repo, ref, per_page: 100
})
for (const cache of caches) {
if (cache.ref !== ref) throw new Error(`Unexpected cache ref: ${cache.ref}`)
try {
await github.rest.actions.deleteActionsCacheById({
...context.repo, cache_id: cache.id
})
} catch (error) {
if (error.status !== 404) throw error
}
}
core.info(`Removed ${caches.length} caches scoped to ${ref}`)
@@ -0,0 +1,65 @@
name: pnpm verification cache pilot
on:
pull_request:
paths: ['.github/workflows/ci-pnpm-verification-pilot.yml']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pnpm-verification-pilot-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ORCA_BACKGROUND_LAUNCH: '1'
jobs:
seed:
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
id: deps
# PR caches are scoped to the PR merge ref; main never restores them.
- uses: actions/cache/save@v5
if: steps.deps.outputs.verification-cache-hit != 'true'
with:
path: ${{ steps.deps.outputs.verification-cache-path }}
key: ${{ steps.deps.outputs.verification-cache-key }}
measure:
needs: seed
name: measure ${{ matrix.runner }} sample ${{ matrix.sample }} cache ${{ matrix.cache }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
sample: [1, 2, 3]
cache: ['false', 'true']
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
id: deps
with:
cache-pnpm-verification: ${{ matrix.cache }}
- name: Validate treatment and frozen install
env:
CACHE_ENABLED: ${{ matrix.cache }}
CACHE_HIT: ${{ steps.deps.outputs.verification-cache-hit }}
run: |
if [ "$CACHE_ENABLED" = true ]; then test "$CACHE_HIT" = true; fi
git diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
node -e "require.resolve('vitest'); require.resolve('electron')"
+33
View File
@@ -0,0 +1,33 @@
name: CI runner demand
on:
schedule:
- cron: '23 4 * * *'
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: ci-runner-demand
cancel-in-progress: false
jobs:
report:
runs-on: ubuntu-slim
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: config/scripts
persist-credentials: false
- name: Measure the previous complete 24 hours
env:
GH_TOKEN: ${{ github.token }}
run: node config/scripts/ci-runner-demand.mjs
- uses: actions/upload-artifact@v7
with:
name: ci-runner-demand-${{ github.run_id }}
path: ci-demand/
retention-days: 30
+55
View File
@@ -0,0 +1,55 @@
name: Shared xterm dependency cache
on:
push:
branches: [main]
paths:
- '.github/workflows/ci-xterm-cache.yml'
- '.github/actions/prepare-xterm-dependencies/**'
- 'config/patches/xterm-upstream.json'
- 'config/scripts/regenerate-xterm-patches.mjs'
- 'config/scripts/xterm-patch-text.mjs'
- 'package.json'
# Refresh after Node patch releases or cache eviction without rebuilding on hits.
schedule:
- cron: '17 5 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: xterm-cache-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ORCA_BACKGROUND_LAUNCH: '1'
jobs:
seed:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: package.json
package-manager-cache: false
- uses: ./.github/actions/prepare-xterm-dependencies
id: cache
- name: Verify and populate dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$RUNNER_TEMP/xterm-patch-build"
- uses: actions/cache/save@v5
if: steps.cache.outputs.cache-hit != 'true'
with:
path: |
${{ runner.temp }}/xterm-patch-build/upstream/.git
${{ runner.temp }}/xterm-patch-build/upstream/node_modules
key: ${{ steps.cache.outputs.cache-key }}
- name: Preserve fallback downloads
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: |
~/.npm
${{ runner.temp }}/xterm-patch-build/upstream/.git
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
+10 -4
View File
@@ -32,9 +32,8 @@ on:
required: false
type: string
schedule:
# Why: GitHub cron uses UTC; these slots map to 10am and 3pm
# America/Phoenix for the default-branch E2E run.
- cron: '0 17,22 * * *'
# One complete daily reference run; targeted PR coverage remains unchanged.
- cron: '0 17 * * *'
jobs:
build:
@@ -201,7 +200,14 @@ jobs:
node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards/assignment.json ci-shards/selected-discovery.json
- name: Run E2E tests (${{ matrix.shard_name }})
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt
env:
PLAYWRIGHT_JSON_OUTPUT_FILE: ci-shards/results.json
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt --reporter=list,json
- name: Summarize E2E failures
if: always()
continue-on-error: true
run: node config/scripts/ci-e2e-failure-summary.mjs ci-shards/results.json
- name: Upload E2E shard assignment
if: always()
+6 -2
View File
@@ -61,7 +61,10 @@ concurrency:
jobs:
verify:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
# Why ARM: 209s of this job is Vitest and nothing here needs x86: no Android SDK, emulator, gradle,
# Hermes or Watchman, no docker, and no artifacts. The Gemfile.lock lists the generic `ruby`
# platform, so frozen bundler installs without an aarch64-linux entry.
runs-on: ubuntu-24.04-arm
env:
# Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts
@@ -177,7 +180,8 @@ jobs:
recording-pin:
name: RPC recording pin
runs-on: ubuntu-latest
# Why ARM: pure Node plus git; the golden comparison masks `platform`.
runs-on: ubuntu-24.04-arm
defaults:
run:
+73 -47
View File
@@ -1,5 +1,5 @@
name: PR Checks
run-name: 'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
run-name: "PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}"
on:
pull_request:
@@ -59,12 +59,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
# Why blob:none: full history is needed for the merge-base diff, but historical
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
# few this job actually reads on demand.
fetch-depth: 0
# Why depth 50 and not 0: every diff below resolves to HEAD^1, so only the merge commit
# and a little slack are needed. Fetching all 8127 refs' commit graph cost ~20s here and
# is charged to the start of all 22 jobs, since they all need this one.
# Why blob:none stays: the sparse tree below is ~7 files, so there are no blobs to
# materialize and no promisor refetch. Measured 9.5s -> 1.6s against 20.7s today.
fetch-depth: 50
filter: blob:none
sparse-checkout: |
/config/scripts/git-pull-request-diff-base.mjs
/package.json
/README.md
/docs/readme/
@@ -79,8 +82,9 @@ jobs:
- name: Reject new root-level files and folders
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node .github/scripts/check-root-directory-entries.mjs "$BASE_SHA" "$HEAD_SHA"
run: |
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
node .github/scripts/check-root-directory-entries.mjs "$DIFF_BASE" HEAD
# The full Git index retains link targets outside the sparse working tree.
- name: Check README local links
@@ -99,12 +103,15 @@ jobs:
id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# Why HEAD^1 and not --merge-base: HEAD is the pull request merge commit, so its first
# parent is the base side already. Computing a merge base instead would require the
# payload base SHA to be in the graph, which is what forced a full-history checkout.
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
# Why --no-renames: name-only rename detection can report only the destination.
# A code file moved under docs/ must still expose its code-side deletion.
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE_SHA" "$HEAD_SHA")"
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
echo "Changed paths:"
printf '%s\n' "$CHANGED"
printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs | tee -a "$GITHUB_OUTPUT"
@@ -116,8 +123,8 @@ jobs:
run: |
set -euo pipefail
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED="$(git diff --name-only --diff-filter=AMCR --merge-base "$BASE" "$HEAD")"
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE")"
CHANGED="$(git diff --name-only --diff-filter=AMCR "$DIFF_BASE" HEAD)"
# Source routes are executable contracts so a test can prove exact
# authorities, exclusions, and sentinels without evaluating workflow shell.
TEST_FILES_JSON="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs)"
@@ -131,7 +138,7 @@ jobs:
# trigger on IME source rather than on a spec name in some route's list.
NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)"
echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE" "$HEAD")"
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
WSL_SOURCE_CHANGED="$(printf '%s\n' "$WSL_CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --wsl-source)"
echo "wsl_source_changed=$WSL_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED"
@@ -148,20 +155,35 @@ jobs:
name: static analysis
needs: [code_paths]
if: needs.code_paths.outputs.static_analysis == 'true'
runs-on: ubuntu-latest
# Why ARM: measured 128s against 172s on ubuntu-latest, with every compute step faster --
# type-aware 24s->15s, anti-slop 28->19s, localization extraction 67->46s, the orcad smoke
# 39->14s. Both lint engines ship linux-arm64 and the Bun target follows process.arch, so
# the whole toolchain resolves. Free for public repositories, same as the typecheck job.
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Why blob:none: full history is needed for the merge-base diff, but historical
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
# few this job actually reads on demand.
fetch-depth: 0
filter: blob:none
# Why depth 50: the gates below diff against HEAD^1, so no merge base is computed and
# the payload base SHA need not be in the graph.
# Why no blob:none here, unlike code_paths: this job checks out all 30,226 files, and
# the filter then forces a second promisor fetch of nearly every blob. Measured 23s
# blobless against ~11s without it.
fetch-depth: 50
persist-credentials: false
# Why two guarded installs: the mixed root+mobile store entry is 537 MB against
# 321 MB for root alone, and restoring it costs 8.6s against 4.6s. Most PRs skip the
# mobile install below, so they were paying 216 MB for packages they never link. The
# root-only key is also the one the hourly warmer reseeds. Only one of these runs.
- uses: ./.github/actions/install-node-dependencies
if: needs.code_paths.outputs.mobile_dependencies != 'true'
with:
native-runtime: node
- uses: ./.github/actions/install-node-dependencies
if: needs.code_paths.outputs.mobile_dependencies == 'true'
with:
native-runtime: node
cache-dependency-path: |
@@ -218,9 +240,9 @@ jobs:
- name: Check VM runtime rollback compatibility
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if git diff --quiet --merge-base "$BASE_SHA" "$HEAD_SHA" -- \
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
if git diff --quiet "$DIFF_BASE" HEAD -- \
src/shared/ephemeral-vm-runtime-store.ts \
src/shared/ephemeral-vm-runtime-feature-store.ts \
src/shared/ephemeral-vm-runtime-rollback-projection.ts \
@@ -252,10 +274,10 @@ jobs:
background: true
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Detection failures run the full check; renames retain the removed input path.
if git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA" > "$RUNNER_TEMP/localization-changes" &&
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" &&
scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then
echo "Localization extraction inputs are unchanged."
else
@@ -268,11 +290,11 @@ jobs:
- name: Boot orcad and round-trip a terminal
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
ORCA_BACKGROUND_LAUNCH: '1'
run: |
# Detection failures run the smoke; renames retain the removed input path.
if git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA" > "$RUNNER_TEMP/orcad-smoke-changes" &&
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/orcad-smoke-changes" &&
scope="$(node config/scripts/orcad-terminal-smoke-change-scope.mjs "$RUNNER_TEMP/orcad-smoke-changes")" && [ "$scope" = false ]; then
echo "Orcad terminal smoke inputs are unchanged."
else
@@ -413,7 +435,8 @@ jobs:
name: Codex index-heal contract
needs: [code_paths]
if: needs.code_paths.outputs.codex_index_heal_contract == 'true'
runs-on: ubuntu-latest
# Why ARM: @openai/codex ships @openai/codex-linux-arm64.
runs-on: ubuntu-24.04-arm
env:
CODEX_CLI_VERSION: '0.150.1'
@@ -448,7 +471,10 @@ jobs:
name: xterm patch sync
needs: [code_paths]
if: needs.code_paths.outputs.xterm_patch_sync == 'true'
runs-on: ubuntu-latest
# Why ARM: the patch check rebuilds 4 packages x 2 builds and byte-compares against the
# checked-in bundles. Those were generated on Linux x64 and reproduce byte-for-byte on
# darwin-arm64, so the output is neither host-arch nor host-OS dependent.
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
@@ -456,21 +482,14 @@ jobs:
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
# Why: the check rebuilds every package in the manifest from a pinned upstream
# commit — @xterm/xterm and its three addons, each built twice (once unmodified to
# prove the toolchain still reproduces the published bundles, once patched). Caching
# the npm metadata and the shallow clone keeps the repeated cost to the builds
# themselves; the key is the manifest, so a commit, package or toolchain bump
# invalidates it.
- name: Restore upstream xterm build inputs
uses: actions/cache@v5
# The generator uses Node built-ins and installs its own upstream toolchain.
- uses: actions/setup-node@v6
with:
path: |
~/.npm
${{ runner.temp }}/xterm-patch-build/upstream/.git
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
node-version-file: package.json
package-manager-cache: false
# Rebuild both pristine and patched bundles; reuse only the pinned toolchain.
- uses: ./.github/actions/prepare-xterm-dependencies
- name: Verify xterm patches match the pinned upstream build
env:
@@ -481,7 +500,8 @@ jobs:
name: shell contracts
needs: [code_paths]
if: needs.code_paths.outputs.shell_contracts == 'true'
runs-on: ubuntu-latest
# Why ARM: fish 4.x is published for noble/arm64 and zsh is in the arm64 archive.
runs-on: ubuntu-24.04-arm
# Why: this job's cost is almost entirely package download, and a stalled mirror has
# no wall-clock bound of its own. A successful run finishes in ~4.5 minutes, so this
# is generous; it exists so a wedge fails the job instead of holding the whole run
@@ -620,16 +640,19 @@ jobs:
node-version: '24'
test:
needs: [code_paths, test_native_cache]
needs: [code_paths, test_native_cache, static_analysis, typecheck]
# Honor cancellation while allowing the optional native-cache primer to skip.
if: >-
!cancelled() &&
needs.code_paths.outputs.test == 'true' &&
needs.static_analysis.result == 'success' &&
needs.typecheck.result == 'success' &&
(needs.test_native_cache.result == 'success' || needs.test_native_cache.result == 'skipped')
uses: ./.github/workflows/unit-tests.yml
with:
node_versions: '["24"]'
runner: ubuntu-24.04-arm
selection_mode: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
# Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
# would pay for it on every shard to run one file in whichever shard it landed in.
@@ -751,7 +774,8 @@ jobs:
name: cross-version wire compatibility
needs: [code_paths]
if: needs.code_paths.outputs.cross-version-wire == 'true'
runs-on: ubuntu-latest
# Why ARM: source-only: tagged checkout plus in-process vitest, no docker or browser.
runs-on: ubuntu-24.04-arm
steps:
# Why fetch-depth 0: the harness extracts the newest release tag to skew
@@ -783,6 +807,7 @@ jobs:
tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
tests/e2e/cross-version-wire/cross-version-worktree-identity-downgrade.unit.test.ts
tests/e2e/cross-version-wire/cross-version-session-tabs-retirement-proof.unit.test.ts
tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts
tests/e2e/cross-version-wire/agent-session-unproven-release-downgrade.unit.test.ts
tests/e2e/cross-version-wire/cross-version-worktree-ps-verdict.unit.test.ts
@@ -790,7 +815,8 @@ jobs:
name: managed hooks on Node 18
needs: [code_paths]
if: needs.code_paths.outputs.managed_hook_node18 == 'true'
runs-on: ubuntu-latest
# Why ARM: Node 18 publishes linux-arm64; the per-platform runtime files are read as data.
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
@@ -813,7 +839,7 @@ jobs:
package:
name: package
needs: [code_paths]
needs: [code_paths, static_analysis, typecheck]
if: needs.code_paths.outputs.package == 'true'
runs-on: ubuntu-latest
# Let the serial Docker gates reach their own deadlines and report cleanup failures.
@@ -826,7 +852,7 @@ jobs:
persist-credentials: false
- name: Cache electron-builder downloads
uses: actions/cache@v5
uses: actions/cache/restore@v5
with:
path: ~/.cache/electron-builder
key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }}
@@ -961,7 +987,7 @@ jobs:
package_windows:
name: package (windows)
needs: [code_paths]
needs: [code_paths, static_analysis, typecheck]
if: needs.code_paths.outputs.package_windows == 'true'
runs-on: windows-2022
timeout-minutes: 30
+52 -2
View File
@@ -1,6 +1,6 @@
# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED
# Explicit review identities share workflow concurrency; legacy names use ordered cancellation.
name: Pullfrog
run-name: ${{ inputs.name || github.workflow }}
run-name: ${{ inputs.name || github.workflow }}${{ inputs.pull_request_number && format(' | PR {0}', inputs.pull_request_number) || '' }}
on:
workflow_dispatch:
inputs:
@@ -11,21 +11,71 @@ on:
type: string
description: Run name
pull_request_number:
type: string
description: Optional PR identity for cancelling superseded reviews
head_sha:
type: string
description: Optional expected PR head; stale reviews are skipped
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ inputs.pull_request_number && format('pullfrog-pr-{0}', inputs.pull_request_number) || format('pullfrog-run-{0}', github.run_id) }}
cancel-in-progress: true
jobs:
review_scope:
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: read
actions: write
outputs:
current: ${{ steps.scope.outputs.current }}
number: ${{ steps.scope.outputs.number }}
head: ${{ steps.scope.outputs.head }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: config/scripts/pullfrog-review-scope.cjs
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/github-script@v8
id: scope
with:
script: |
const { reviewScope } = require('./config/scripts/pullfrog-review-scope.cjs')
await reviewScope({ github, context, core })
pullfrog:
needs: review_scope
if: needs.review_scope.outputs.current == 'true'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Recheck review head before starting agent
id: freshness
if: needs.review_scope.outputs.number != ''
uses: actions/github-script@v8
env:
REVIEW_NUMBER: ${{ needs.review_scope.outputs.number }}
REVIEW_HEAD: ${{ needs.review_scope.outputs.head }}
with:
script: |
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: Number(process.env.REVIEW_NUMBER) })
core.setOutput('current', pr.state === 'open' && pr.head.sha === process.env.REVIEW_HEAD)
- name: Run agent
if: needs.review_scope.outputs.number == '' || steps.freshness.outputs.current == 'true'
uses: pullfrog/pullfrog@v0
with:
prompt: ${{ inputs.prompt }}
+11 -73
View File
@@ -19,80 +19,18 @@ jobs:
runs-on: ubuntu-slim
timeout-minutes: 5
steps:
# Why: release events run this file from the tagged commit, so load the module from the same commit.
- uses: actions/checkout@v6
with:
sparse-checkout: config/scripts/release-policy.mjs
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Enforce release policy
uses: actions/github-script@v8
with:
script: |
const release = context.payload.release;
const tag = release.tag_name;
const author = release.author?.login;
const number = "(?:0|[1-9][0-9]*)";
const version = `${number}\\.${number}\\.${number}`;
const stableTag = new RegExp(`^v${version}$`);
const prereleaseTag = new RegExp(
`^(?:v${version}-rc\\.${number}(?:\\.[0-9A-Za-z]+)?|mobile(?:-android)?-v${version})$`,
);
const expectedPrerelease = prereleaseTag.test(tag);
const allowed = author === "github-actions[bot]" &&
(stableTag.test(tag) || expectedPrerelease);
const { owner, repo } = context.repo;
async function restoreLatestStable() {
const releases = await github.paginate(github.rest.repos.listReleases, {
owner,
repo,
per_page: 100,
});
const stable = releases
.filter((candidate) =>
!candidate.draft &&
!candidate.prerelease &&
candidate.author?.login === "github-actions[bot]" &&
stableTag.test(candidate.tag_name),
)
.sort((left, right) => {
const a = left.tag_name.slice(1).split(".").map(Number);
const b = right.tag_name.slice(1).split(".").map(Number);
return a.reduce((result, part, index) => result || part - b[index], 0);
})
.at(-1);
if (stable) {
await github.rest.repos.updateRelease({
owner,
repo,
release_id: stable.id,
make_latest: "true",
});
}
}
if (allowed) {
if (release.prerelease !== expectedPrerelease) {
await github.rest.repos.updateRelease({
owner,
repo,
release_id: release.id,
prerelease: expectedPrerelease,
make_latest: expectedPrerelease ? "false" : "legacy",
});
}
await restoreLatestStable();
return;
}
await github.rest.repos.updateRelease({
owner,
repo,
release_id: release.id,
draft: true,
prerelease: true,
make_latest: "false",
});
await restoreLatestStable();
await github.rest.repos.deleteRelease({ owner, repo, release_id: release.id });
try {
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
} catch (error) {
if (error.status !== 404) throw error;
}
core.warning(`Deleted unauthorized release ${tag} created by ${author || "unknown"}.`);
const { pathToFileURL } = await import("node:url")
const { enforceReleasePolicy } = await import(
pathToFileURL(`${process.env.GITHUB_WORKSPACE}/config/scripts/release-policy.mjs`).href
)
await enforceReleasePolicy({ github, context, core })
+69 -25
View File
@@ -14,19 +14,48 @@ on:
default: ubuntu-latest
type: string
selection_mode:
description: Shadow validates selection; selected applies it only to draft PRs.
required: false
default: shadow
type: string
permissions:
contents: read
jobs:
plan:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
shards: ${{ steps.plan.outputs.shards }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- name: Plan unit selection
id: plan
env:
ORCA_UNIT_SELECTION_MODE: ${{ inputs.selection_mode }}
run: node config/scripts/ci-unit-plan.mjs
- uses: actions/upload-artifact@v7
continue-on-error: true
with:
name: unit-selection-attempt-${{ github.run_attempt }}
path: ci-shards/unit-selection.json
retention-days: 14
test:
name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }}
needs: plan
name: tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }}
runs-on: ${{ inputs.runner }}
strategy:
fail-fast: false
matrix:
node: ${{ fromJSON(inputs.node_versions) }}
shard: [1, 2, 3, 4, 5, 6, 7, 8]
shard_total: [8]
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
steps:
- name: Checkout
@@ -43,6 +72,12 @@ jobs:
- name: Install Electron package binary for tests
run: node config/scripts/install-electron-package-binary.mjs
- uses: actions/download-artifact@v8
continue-on-error: true
with:
name: unit-selection-attempt-${{ github.run_attempt }}
path: ci-shards/
- name: Test shard
env:
ORCA_BALANCE_UNIT_SHARDS: '1'
@@ -50,27 +85,7 @@ jobs:
run: |
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
pnpm exec vitest run --config config/vitest.config.ts \
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
--exclude=src/main/daemon/shell-ready.test.ts \
--exclude=src/main/daemon/node-pty-fd-leak.test.ts \
--exclude=src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
--exclude=src/main/pty/omp-shell-wrapper-alias-safety.test.ts \
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
--exclude=src/main/shell-startup-feature-channel.test.ts \
--exclude=src/main/terminal-history-fish-session.node-pty.test.ts \
--exclude=src/main/zsh-scoped-histfile.live-shell.test.ts \
--exclude=src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \
--exclude=src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
--exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
--exclude=src/shared/fish-query-reply-child-stdin.node-pty.test.ts \
--exclude=src/shared/pty-reply-echo-shapes.node-pty.test.ts \
--exclude=src/shared/startup-shell-portability.live-shell.test.ts \
--exclude=src/shared/posix-command-path-lookup.test.ts \
--exclude=tests/e2e/relay-region-compatibility.unit.test.ts \
--exclude=tests/e2e/relay-region-correction.unit.test.ts \
--exclude=tests/e2e/cross-version-wire/** \
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }}
- name: Upload unit shard assignment
if: always()
@@ -78,11 +93,40 @@ jobs:
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }}
path: ci-shards/
retention-days: 14
if-no-files-found: warn
selection_evidence:
needs: test
if: ${{ !cancelled() }}
continue-on-error: true
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: config/scripts/ci-unit-selection-review.mjs
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: actions/download-artifact@v8
with:
pattern: unit-shard-node-*-attempt-${{ github.run_attempt }}
path: unit-evidence/
- name: Compare selection with full results
continue-on-error: true
run: node config/scripts/ci-unit-selection-review.mjs unit-evidence
- uses: actions/upload-artifact@v7
if: always()
continue-on-error: true
with:
name: unit-selection-review-attempt-${{ github.run_attempt }}
path: unit-evidence/selection-review.json
retention-days: 30
relay_integration:
name: relay integration node ${{ matrix.node }}
strategy:
+4 -2
View File
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -179,6 +179,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="MiMo Code logo" width="16" valign="middle" /> MiMo Code</kbd></a> &nbsp;
@@ -195,6 +196,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com"><kbd><img src="src/shared/agent-icons/freebuff.png" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="docs/assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA)
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [Download APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
---
+1
View File
@@ -0,0 +1 @@
[]
@@ -0,0 +1,8 @@
const release = require('./electron-builder.config.cjs')
// CI discards these packages after smoke tests; keep release compression unchanged.
module.exports = {
...release,
deb: { ...release.deb, fpm: [...(release.deb.fpm ?? []), '--deb-compression-level=1'] },
rpm: { ...release.rpm, fpm: [...(release.rpm.fpm ?? []), '--rpm-compression-level=1'] }
}
+17 -3
View File
@@ -8,6 +8,7 @@ import {
ORCAD_ENTRY_POINT
} from './orcad-entry-build.mjs'
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
import { bunProfileQualification } from './bun-profile-qualification.mjs'
const ROOT = resolve(import.meta.dirname, '../..')
const BUILD_SCRIPTS = [
@@ -29,7 +30,9 @@ const ALWAYS_FILES = new Set([
'tsconfig.json',
'.github/workflows/bun-profile-tests.yml',
'config/scripts/bun-profile-change-scope.mjs',
'config/scripts/bun-profile-change-scope.test.mjs'
'config/scripts/bun-profile-change-scope.test.mjs',
'config/scripts/bun-profile-qualification.mjs',
'config/scripts/bun-profile-qualification.test.mjs'
])
const ALWAYS_PREFIXES = [
'.github/actions/install-node-dependencies/',
@@ -110,7 +113,11 @@ export async function classifyBunProfileChanges(changedFiles, collect = collectB
reason: matched ? `Runtime or test dependency changed: ${matched}` : 'No Bun inputs changed'
}
} catch (error) {
return { shouldRun: true, reason: `Dependency graph unavailable: ${String(error)}` }
return {
shouldRun: true,
graphUnavailable: true,
reason: `Dependency graph unavailable: ${String(error)}`
}
}
}
@@ -118,7 +125,14 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
const changedFiles = readFileSync(process.argv[2], 'utf8').split('\0').filter(Boolean)
const result = await classifyBunProfileChanges(changedFiles)
console.log(result.reason)
const output = `should_run=${String(result.shouldRun)}\n`
let event = {}
try {
event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))
} catch {
// Missing event evidence retains full qualification.
}
const policy = bunProfileQualification(changedFiles, result, event)
const output = `should_run=${result.shouldRun}\nqualification=${policy.qualification}\nrunners=${JSON.stringify(policy.runners)}\n`
if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, output)
} else {
@@ -78,6 +78,7 @@ it.each([
'.npmrc',
'tsconfig.json',
'config/tsconfig.node.json',
'config/scripts/bun-profile-qualification.mjs',
'config/patches/node-pty@1.1.0.patch',
'native/windows-registry/src/addon.cc',
'.github/actions/install-node-dependencies/action.yml',
@@ -140,12 +141,15 @@ it('keeps all ten platform jobs and runs them when detection is skipped or fails
expect(workflow.jobs.changes.steps[0].with['persist-credentials']).toBe(false)
const detect = workflow.jobs.changes.steps.find((step) => step.id === 'scope')
expect(detect.run).toContain('git diff --name-only --no-renames -z HEAD^1 HEAD')
let count = 0
for (const jobName of ['persistence', 'linux_glibc_floor', 'linux_musl']) {
expect(workflow.on.pull_request.types).toContain('ready_for_review')
expect(workflow.on.schedule).toHaveLength(1)
expect(workflow.jobs.persistence.strategy.matrix.os).toContain('needs.changes.outputs.runners')
for (const jobName of ['linux_glibc_floor', 'linux_musl']) {
const job = workflow.jobs[jobName]
expect(job.needs).toBe('changes')
expect(job.if).toBe("${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}")
count += job.strategy.matrix.os.length
expect(job.needs).toEqual(['changes', 'persistence'])
expect(job.if).toContain("needs.persistence.result == 'success'")
expect(job.if).toContain("needs.changes.outputs.qualification != 'false'")
expect(job.if).toContain("needs.changes.outputs.should_run != 'false'")
expect(job.strategy.matrix.os).toEqual(['ubuntu-22.04', 'ubuntu-24.04-arm'])
}
expect(count).toBe(10)
})
@@ -0,0 +1,42 @@
export const BUN_PERSISTENCE_RUNNERS = [
'ubuntu-22.04',
'ubuntu-24.04-arm',
'macos-14',
'macos-15-intel',
'windows-2022',
'windows-11-arm'
]
const QUALIFICATION_PREFIXES = [
'config/',
'native/',
'resources/',
'.github/',
'src/main/persistence/',
'src/main/sqlite/',
'src/main/orcad/',
'src/main/providers/',
'src/main/daemon/',
'src/main/ssh/',
'src/relay/',
'src/shared/child-process/'
]
export function bunProfileQualification(changedFiles, scope, event = {}) {
const sensitive = changedFiles.some(
(file) =>
!file.includes('/') ||
QUALIFICATION_PREFIXES.some((prefix) => file.startsWith(prefix)) ||
/(?:^|[/.-])(?:windows|win32|wsl|macos|darwin|linux|posix|bun)(?:[/.-]|$)/i.test(file)
)
// Only a proven unrelated platform change in a draft may defer qualification.
const full =
event.pull_request?.draft !== true ||
changedFiles.length === 0 ||
scope.graphUnavailable === true ||
sensitive
return {
qualification: full,
runners: full ? BUN_PERSISTENCE_RUNNERS : ['ubuntu-22.04']
}
}
@@ -0,0 +1,47 @@
import { expect, it } from 'vitest'
import { BUN_PERSISTENCE_RUNNERS, bunProfileQualification } from './bun-profile-qualification.mjs'
const draft = { pull_request: { draft: true } }
const scope = { shouldRun: true }
it('defers only platform qualification for ordinary draft runtime changes', () => {
expect(
bunProfileQualification(['src/main/runtime/rpc/methods/example.ts'], scope, draft)
).toEqual({
qualification: false,
runners: ['ubuntu-22.04']
})
})
it.each([
'package.json',
'native/windows-registry/src/addon.cc',
'config/vitest.config.ts',
'src/main/ssh/ssh-provider.ts',
'src/main/providers/local-pty-provider.ts',
'src/shared/child-process/run-process.ts',
'src/main/persistence/profile-state/store.ts',
'src/main/sqlite/database.ts',
'src/main/orcad/entry.ts',
'src/main/runtime/windows-terminal.ts',
'src/shared/linux-glibc.ts',
'src/main/daemon/entry.ts',
'src/relay/index.ts',
'src/main/wsl/runner.ts'
])('retains all platforms for sensitive input %s', (file) => {
expect(bunProfileQualification([file], scope, draft)).toEqual({
qualification: true,
runners: BUN_PERSISTENCE_RUNNERS
})
})
it('qualifies every ready commit, scheduled/manual runs and incomplete evidence', () => {
const paths = ['src/main/runtime/rpc/methods/example.ts']
for (const event of [{}, { pull_request: { draft: false } }]) {
expect(bunProfileQualification(paths, scope, event).qualification).toBe(true)
}
expect(bunProfileQualification([], scope, draft).qualification).toBe(true)
expect(
bunProfileQualification(paths, { ...scope, graphUnavailable: true }, draft).qualification
).toBe(true)
})
+13 -6
View File
@@ -136,9 +136,14 @@ function resolveBase(root, requestedBase) {
}
export function collectAddedLineRanges(root, requestedBase) {
const base = resolveBase(root, requestedBase)
const mergeBase = runGit(root, ['merge-base', base, 'HEAD']).trim()
const comparisonBase = resolvePullRequestDiffBase(root, mergeBase)
// On a pull_request checkout HEAD is the merge commit, so its first parent is the base side and
// no merge base has to be computed. Resolving it first is what lets CI checkout shallowly: the
// payload base SHA can lag HEAD^1 by any number of commits and need not be in the graph at all.
// Off that ref (local runs) the requested base is an arbitrary branch tip, so the merge base is
// still what isolates this branch's own lines.
const comparisonBase =
resolvePullRequestDiffBase(root, null) ??
runGit(root, ['merge-base', resolveBase(root, requestedBase), 'HEAD']).trim()
const changedFiles = splitNullDelimited(
runGit(root, ['diff', '--name-only', '-z', '--diff-filter=ACMRTUB', comparisonBase, '--'])
)
@@ -174,7 +179,7 @@ export function collectAddedLineRanges(root, requestedBase) {
const lineCount = readFileSync(absolutePath, 'utf8').split(/\r?\n/).length
rangesByFile.set(file, [{ start: 1, end: lineCount }])
}
return { base, comparisonBase, rangesByFile }
return { comparisonBase, rangesByFile }
}
function parseOxlintOutput(stdout, label) {
@@ -414,10 +419,12 @@ export function main(
root = process.cwd(),
requestedBase = process.argv.slice(2).find((argument) => argument !== '--')
) {
const { base, comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase)
const { comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase)
const files = [...rangesByFile.keys()]
if (files.length === 0) {
console.log(`Changed-code quality gate: no changed JavaScript or TypeScript since ${base}.`)
console.log(
`Changed-code quality gate: no changed JavaScript or TypeScript since ${comparisonBase.slice(0, 12)}.`
)
return 0
}
@@ -228,8 +228,14 @@ describe('root directory guard', () => {
expect(guardJob.if).toBeUndefined()
expect(guardStep.if).toBeUndefined()
expect(guardJob.steps[0].with['fetch-depth']).toBe(0)
// Why >= 2 rather than 0: the guard compares the merge commit's first parent against the
// merged tree, so it needs both parents present but no history beyond them. Depth 1 would
// leave HEAD^1 unreachable and the guard would fail closed on every run.
expect(guardJob.steps[0].with['fetch-depth']).toBeGreaterThanOrEqual(2)
expect(guardStep.run).toContain('node .github/scripts/check-root-directory-entries.mjs')
// The base side must come from the merge ref, not the event payload, or a shallow checkout
// cannot resolve it.
expect(guardStep.run).toContain('git-pull-request-diff-base.mjs')
expect(workflow.jobs.verify.needs).toContain('code_paths')
})
})
@@ -107,7 +107,7 @@ describe('CI background step barriers', () => {
it('joins package setup before reading outputs and preserves isolated native probes', () => {
const steps = pr.jobs.package.steps
// Parallel composites must not race to download their shared cache action on first use.
const cacheAction = steps.findIndex((step) => step.uses === 'actions/cache@v5')
const cacheAction = steps.findIndex((step) => step.uses === 'actions/cache/restore@v5')
expect(cacheAction).toBeGreaterThanOrEqual(0)
expect(cacheAction).toBeLessThan(
steps.findIndex((step) => step.id === 'shutdown-fixture-cache')
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import { expect, it } from 'vitest'
import { parse } from 'yaml'
import { BUN_PERSISTENCE_RUNNERS } from './bun-profile-qualification.mjs'
const readWorkflow = (name) =>
parse(readFileSync(new URL(`../../.github/workflows/${name}.yml`, import.meta.url), 'utf8'))
@@ -15,7 +16,21 @@ it('warms the same Linux Node runtime the PR shards restore', () => {
const primer = readWorkflow('pr').jobs.test_native_cache
expect(arm['runs-on']).toBe(primer['runs-on'])
expect(arm.steps.at(-1).run).toBe('node config/scripts/ensure-native-runtime.mjs --check-only')
expect(install.with).toEqual(primer.steps.find((step) => step.uses === install.uses).with)
expect(install.with).toMatchObject(primer.steps.find((step) => step.uses === install.uses).with)
})
it('populates shared Electron archives on both Linux architectures without changing the Node ABI', () => {
for (const name of ['warm', 'warm-linux-arm']) {
const steps = workflow.jobs[name].steps
const install = steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(install.with['native-runtime']).toBe('node')
expect(install.with['cache-electron-package']).toBe('true')
const populate = steps.find((step) => step.name === 'Populate shared Electron archive')
expect(populate.run).toBe('node config/scripts/install-electron-package-binary.mjs')
expect(steps.indexOf(populate)).toBeGreaterThan(steps.indexOf(install))
}
})
it('publishes incremental state under a key and prefix that new PRs restore', () => {
@@ -50,9 +65,8 @@ it('bounds warming to the required platforms and validates changes without grant
it('warms and probes both Windows images with the persistence job runtime', () => {
const job = workflow.jobs['warm-windows']
const persistence = readWorkflow('bun-profile-tests').jobs.persistence
expect(job.strategy.matrix.os).toEqual(
persistence.strategy.matrix.os.filter((os) => os.startsWith('windows-'))
BUN_PERSISTENCE_RUNNERS.filter((os) => os.startsWith('windows-'))
)
expect(job['runs-on']).toBe('${{ matrix.os }}')
expect(job.strategy['fail-fast']).toBe(false)
@@ -0,0 +1,57 @@
import { readFileSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
import { expect, it, vi } from 'vitest'
import { parse } from 'yaml'
const workflow = parse(readFileSync('.github/workflows/ci-closed-pr-caches.yml', 'utf8'))
const script = workflow.jobs.clean.steps[0].with.script
const ref = 'refs/pull/123/merge'
function run(caches, remove = vi.fn().mockResolvedValue(undefined)) {
const list = vi.fn()
const paginate = vi.fn().mockResolvedValue(caches)
const result = runInNewContext(`(async () => { ${script} })()`, {
context: { repo: { owner: 'owner', repo: 'repo' }, payload: { pull_request: { number: 123 } } },
github: {
paginate,
rest: { actions: { getActionsCacheList: list, deleteActionsCacheById: remove } }
},
core: { info: vi.fn() }
})
return { result, remove, paginate, list }
}
it('uses default-branch code without checking out a closed PR', () => {
expect(workflow.on).toEqual({ pull_request_target: { types: ['closed'] } })
expect(workflow.permissions).toEqual({ actions: 'write' })
expect(workflow.jobs.clean.steps).toHaveLength(1)
expect(workflow.jobs.clean.steps[0].uses).toBe('actions/github-script@v8')
})
it('lists and deletes only caches scoped to the closed merge ref', async () => {
const { result, paginate, list, remove } = run([
{ id: 12, ref },
{ id: 13, ref }
])
await result
expect(paginate).toHaveBeenCalledWith(list, { owner: 'owner', repo: 'repo', ref, per_page: 100 })
expect(remove.mock.calls).toEqual([
[{ owner: 'owner', repo: 'repo', cache_id: 12 }],
[{ owner: 'owner', repo: 'repo', cache_id: 13 }]
])
})
it('refuses a default-branch cache even if the API returns one', async () => {
const { result, remove } = run([{ id: 12, ref: 'refs/heads/main' }])
await expect(result).rejects.toThrow('Unexpected cache ref')
expect(remove).not.toHaveBeenCalled()
})
it('tolerates an eviction race but surfaces permission failures', async () => {
await expect(
run([{ id: 12, ref }], vi.fn().mockRejectedValue({ status: 404 })).result
).resolves.toBeUndefined()
await expect(
run([{ id: 12, ref }], vi.fn().mockRejectedValue({ status: 403 })).result
).rejects.toEqual({ status: 403 })
})
@@ -48,7 +48,11 @@ describe('CI dependency download caches', () => {
action.runs.steps.findIndex((step) => step.name === 'Install dependencies')
)
const saves = action.runs.steps.filter((step) => step.uses === 'actions/cache/save@v5')
expect(saves).toEqual([])
expect(saves).toHaveLength(1)
expect(saves[0].name).toBe('Save pnpm verification record on main')
expect(saves[0].if).toContain("github.ref == 'refs/heads/main'")
expect(saves[0].if).toContain("github.event_name != 'pull_request'")
expect(saves[0].with.path).toBe('${{ steps.verification-cache.outputs.path }}')
})
it('restores Windows packaging downloads from the release cache without a PR upload', () => {
@@ -99,8 +103,8 @@ describe('CI dependency download caches', () => {
expect(writer.with['restore-keys']).toBeUndefined()
expect(writer.with['lookup-only']).toBe(true)
expect(release.steps.indexOf(writer)).toBeGreaterThan(release.steps.indexOf(combined))
// Retain PR fallback saves until a successful release seeds the default-branch entry.
expect(consumer.uses).toBe('actions/cache@v5')
expect(consumer.uses).toBe('actions/cache/restore@v5')
expect(consumer.with['restore-keys'].trim()).toBe('electron-builder-linux-')
expect(combined.uses).toBe('actions/cache@v5')
expect(linux.eb_cache_path.trim().split('\n')).toEqual([
'~/.cache/electron',
@@ -109,6 +113,20 @@ describe('CI dependency download caches', () => {
})
})
it('shares Electron archives with PRs without uploading PR-local copies', () => {
const save = action.runs.steps.find((step) => step.name === 'Cache Electron package archive')
const restore = action.runs.steps.find(
(step) => step.name === 'Restore Electron package archive without saving'
)
expect(save.if).toContain("github.event_name != 'pull_request' || runner.os != 'Linux'")
expect(restore.if).toContain("github.event_name == 'pull_request' && runner.os == 'Linux'")
expect(save.if).toContain("steps.electron-package-cache.outputs.version != ''")
expect(restore.if).toContain("steps.electron-package-cache.outputs.version != ''")
expect(save.uses).toBe('actions/cache@v5')
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore.with).toEqual(save.with)
})
describe('release install targets', () => {
const macCpuFlag = '--cpu=current,x64,arm64'
// Both shapes: `run:` steps and steps wrapped in nick-fields/retry (`with.command`).
+92
View File
@@ -0,0 +1,92 @@
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { trackE2eFailures } from './ci-e2e-failure-tracking.mjs'
export function e2eFailureSummary(report) {
const failures = []
let passed = 0
let skipped = 0
function visit(suite, parents = [], parentFile) {
const titles = [...parents, suite.title].filter(Boolean)
const file = suite.file ?? parentFile
for (const spec of suite.specs ?? []) {
for (const test of spec.tests ?? []) {
if (test.status === 'expected') {
passed++
continue
}
if (test.status === 'skipped') {
skipped++
continue
}
const errors = test.results.flatMap((result) => result.errors ?? [])
failures.push({
file: spec.file ?? file,
title: [...titles, spec.title].join(' › '),
project: test.projectName,
status: test.status,
message: errors
.map((error) => error.message ?? error.value ?? '')
.join('\n')
.slice(0, 2000)
})
}
}
for (const child of suite.suites ?? []) {
visit(child, titles, file)
}
}
for (const suite of report.suites ?? []) {
visit(suite)
}
return { passed, skipped, failures, errors: report.errors ?? [] }
}
export function renderE2eFailures(summary, records = [], now = new Date()) {
const escape = (value) =>
String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
const tracked = trackE2eFailures(summary.failures, records, now)
const details = (failure) =>
`<details><summary>${escape(failure.status)}: ${escape(failure.file)} — ${escape(failure.title)}</summary><pre>${escape(failure.message)}</pre></details>`
return [
'## E2E results',
'',
`${summary.passed} expected results; ${summary.skipped} skipped; ${summary.failures.length} unexpected/flaky results; ${summary.errors.length} run errors.`,
'',
`### Untracked failures (${tracked.untracked.length})`,
'',
...tracked.untracked.map(details),
'',
`### Tracked failures (${tracked.known.length})`,
'',
...tracked.known.flatMap((failure) => [
details(failure),
`Owner: ${escape(failure.tracking.owner)}; ${escape(failure.tracking.issue)}; expires ${escape(failure.tracking.expires)}.`
]),
...(tracked.invalid.length
? [`${tracked.invalid.length} invalid/expired tracking entries were not used.`]
: []),
...summary.errors.map((error) => `<pre>${escape(error.message ?? error.value)}</pre>`),
'',
'Failures retain their original verdict. Repeated failures need a tracked owner, reproduction and review date; do not treat a red baseline as passing.',
''
].join('\n')
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const summary = e2eFailureSummary(JSON.parse(readFileSync(process.argv[2], 'utf8')))
const records = JSON.parse(
readFileSync(new URL('../e2e-failure-tracking.json', import.meta.url), 'utf8')
)
appendFileSync(process.env.GITHUB_STEP_SUMMARY, renderE2eFailures(summary, records))
} catch (error) {
appendFileSync(
process.env.GITHUB_STEP_SUMMARY,
`E2E report unavailable (${error.code ?? 'invalid report'}); inspect the failing step and traces.\n`
)
}
}
@@ -0,0 +1,79 @@
import { expect, it } from 'vitest'
import { e2eFailureSummary, renderE2eFailures } from './ci-e2e-failure-summary.mjs'
import { trackE2eFailures } from './ci-e2e-failure-tracking.mjs'
it('requires exact failures with an owner, issue and unexpired review date', () => {
const failure = {
file: 'test.spec.ts',
title: 'case',
project: 'electron',
message: 'expected focus failed'
}
const record = {
...failure,
message: 'expected focus',
owner: '@owner',
issue: 'https://github.com/stablyai/orca/issues/123',
expires: '2026-10-01'
}
const now = new Date('2026-09-28')
expect(trackE2eFailures([failure], [null, record], now)).toMatchObject({
known: [{ ...failure, tracking: record }],
invalid: [null]
})
expect(trackE2eFailures([failure], null, now).untracked).toEqual([failure])
for (const change of [
{ expires: '2026-09-27' },
{ expires: '2026-09-31' },
{ owner: '' },
{ issue: '' },
{ title: 'different' },
{ message: 'different' },
{ project: 'other' }
]) {
expect(trackE2eFailures([failure], [{ ...record, ...change }], now).untracked).toEqual([
failure
])
}
})
it('keeps failure, flaky, skipped and startup-error evidence separate', () => {
const report = {
errors: [{ message: 'startup failed' }],
suites: [
{
title: 'file',
file: 'tests/e2e/test.spec.ts',
suites: [
{
title: 'feature',
specs: [
{
title: 'case',
tests: [
{ status: 'expected' },
{ status: 'skipped' },
{
status: 'unexpected',
projectName: 'electron-headless',
results: [{ errors: [{ message: '<timeout>' }] }]
},
{
status: 'flaky',
results: [{ errors: [{ message: 'first try' }] }, { errors: [] }]
}
]
}
]
}
]
}
]
}
const summary = e2eFailureSummary(report)
expect(summary).toMatchObject({ passed: 1, skipped: 1, errors: report.errors })
expect(summary.failures).toHaveLength(2)
expect(summary.failures[0].title).toBe('file › feature › case')
expect(renderE2eFailures(summary)).toContain('&lt;timeout&gt;')
expect(renderE2eFailures(summary)).toContain('startup failed')
})
@@ -0,0 +1,42 @@
export function trackE2eFailures(failures, records, now = new Date()) {
const known = []
const untracked = []
const invalid = []
const active = (Array.isArray(records) ? records : [records]).filter((record) => {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
invalid.push(record)
return false
}
const expiry = new Date(`${record.expires}T23:59:59Z`)
const valid =
typeof record.file === 'string' &&
typeof record.title === 'string' &&
typeof record.message === 'string' &&
record.message.length > 0 &&
/^@[\w-]+(?:\/[\w-]+)?$/.test(record.owner ?? '') &&
/^https:\/\/github\.com\/stablyai\/orca\/issues\/\d+$/.test(record.issue ?? '') &&
/^\d{4}-\d{2}-\d{2}$/.test(record.expires ?? '') &&
Number.isFinite(expiry.getTime()) &&
expiry.toISOString().slice(0, 10) === record.expires &&
expiry.getTime() >= now.getTime()
if (!valid) {
invalid.push(record)
}
return valid
})
for (const failure of failures) {
const record = active.find(
(entry) =>
entry.file === failure.file &&
entry.title === failure.title &&
entry.project === failure.project &&
failure.message.includes(entry.message)
)
if (record) {
known.push({ ...failure, tracking: record })
} else {
untracked.push(failure)
}
}
return { known, untracked, invalid }
}
+100
View File
@@ -0,0 +1,100 @@
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { runnerDemand, sampleWorkflowRuns } from './ci-runner-metrics.mjs'
async function api(path, env) {
const response = await fetch(
`${env.GITHUB_API_URL ?? 'https://api.github.com'}/repos/${env.GITHUB_REPOSITORY}/${path}`,
{
headers: {
Authorization: `Bearer ${env.GH_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
},
signal: AbortSignal.timeout(30_000)
}
)
if (!response.ok) {
throw new Error(`Actions API: ${response.status}`)
}
return response.json()
}
export async function collectRunnerDemand(env = process.env, request = (path) => api(path, env)) {
const end = new Date(env.CI_METRICS_END ?? Date.now())
end.setUTCMinutes(0, 0, 0)
if (!Number.isFinite(end.getTime())) {
throw new Error('Invalid window end')
}
const runs = []
for (let hour = 0; hour < 24; hour++) {
const until = new Date(end.getTime() - hour * 3_600_000 - 1000)
const since = new Date(end.getTime() - (hour + 1) * 3_600_000)
const range = `${since.toISOString()}..${until.toISOString()}`
const path = `actions/runs?per_page=100&created=${encodeURIComponent(range)}`
const first = await request(path)
if (first.total_count > 1000) {
throw new Error('Hourly run inventory exceeds API limit; split the interval')
}
runs.push(...first.workflow_runs)
for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) {
runs.push(...(await request(`${path}&page=${page}`)).workflow_runs)
}
}
const samples = sampleWorkflowRuns(runs)
// Bound API concurrency and preserve every selected observation, including zero-job runs.
for (let index = 0; index < samples.length; index += 4) {
await Promise.all(
samples.slice(index, index + 4).map(async (sample) => {
const path = `actions/runs/${sample.run.id}/jobs?per_page=100`
const first = await request(path)
sample.jobs = first.jobs
for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) {
sample.jobs.push(...(await request(`${path}&page=${page}`)).jobs)
}
})
)
}
const report = {
start: new Date(end.getTime() - 86_400_000).toISOString(),
end: end.toISOString(),
method:
'Stratified by workflow/conclusion, six sampled runs per stratum; latest attempts only; incomplete jobs excluded',
...runnerDemand(runs, samples)
}
return { report, runs, samples }
}
export function demandMarkdown(report) {
return [
`## CI demand: ${report.start} to ${report.end}`,
'',
`Estimated full job duration for runs created in this window (not window-clipped occupancy); ${report.sampledRuns}/${report.populationRuns} runs sampled. ${report.method}.`,
'',
'| Workflow | Runs | Runner hours | Cancelled-run hours | Minutes/completed PR run |',
'| --- | ---: | ---: | ---: | ---: |',
...report.workflows.map(
(row) =>
`| ${row.workflow} | ${row.runs} | ${(row.runnerMinutes / 60).toFixed(1)} | ${(row.cancelledRunnerMinutes / 60).toFixed(1)} | ${row.runnerMinutesPerCompletedPrRun?.toFixed(1) ?? '—'} |`
),
'',
'| Runner labels | Hours | Queue/provisioning p95 minutes |',
'| --- | ---: | ---: |',
...report.pools.map(
(pool) =>
`| ${pool.label} | ${(pool.runnerMinutes / 60).toFixed(1)} | ${pool.queueP95Minutes?.toFixed(1) ?? '—'} |`
),
''
].join('\n')
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const evidence = await collectRunnerDemand()
mkdirSync('ci-demand', { recursive: true })
writeFileSync('ci-demand/evidence.json', JSON.stringify(evidence))
const markdown = demandMarkdown(evidence.report)
writeFileSync('ci-demand/report.md', markdown)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown)
}
}
+110
View File
@@ -0,0 +1,110 @@
const minutesBetween = (start, end) => {
const value = (Date.parse(end) - Date.parse(start)) / 60_000
return Number.isFinite(value) ? Math.max(0, value) : 0
}
const workflowPath = (run) => run.path.split('@')[0]
const workflowKey = (run) => run.workflow_id ?? workflowPath(run)
export function weightedPercentile(values, percentile) {
const sorted = [...values].sort((a, b) => a.value - b.value)
const target = sorted.reduce((sum, item) => sum + item.weight, 0) * percentile
let seen = 0
for (const item of sorted) {
seen += item.weight
if (seen >= target) {
return item.value
}
}
return null
}
export function runnerDemand(runs, samples) {
const workflows = new Map()
const pools = new Map()
for (const run of runs) {
if (!workflows.has(workflowKey(run))) {
workflows.set(workflowKey(run), {
workflow: workflowPath(run),
runs: 0,
sampledRuns: 0,
runnerMinutes: 0,
cancelledRunnerMinutes: 0,
completedPrRuns: 0,
completedPrRunnerMinutes: 0
})
}
workflows.get(workflowKey(run)).runs++
}
let incompleteJobs = 0
for (const { run, jobs, weight } of samples) {
const row = workflows.get(workflowKey(run))
row.sampledRuns++
for (const job of jobs) {
if (!job.runner_name || !job.started_at) {
continue
}
if (!job.completed_at) {
incompleteJobs++
continue
}
const duration = minutesBetween(job.started_at, job.completed_at)
const weighted = duration * weight
row.runnerMinutes += weighted
if (run.conclusion === 'cancelled') {
row.cancelledRunnerMinutes += weighted
}
if (run.event === 'pull_request' && ['success', 'failure'].includes(run.conclusion)) {
row.completedPrRunnerMinutes += weighted
}
const label = [...job.labels].sort().join(', ') || 'unknown'
if (!pools.has(label)) {
pools.set(label, { label, runnerMinutes: 0, waits: [] })
}
const pool = pools.get(label)
pool.runnerMinutes += weighted
if (job.created_at) {
pool.waits.push({ value: minutesBetween(job.created_at, job.started_at), weight })
}
}
if (run.event === 'pull_request' && ['success', 'failure'].includes(run.conclusion)) {
row.completedPrRuns += weight
}
}
return {
populationRuns: runs.length,
sampledRuns: samples.length,
incompleteJobs,
workflows: [...workflows.values()]
.map((row) => ({
...row,
runnerMinutesPerCompletedPrRun: row.completedPrRuns
? row.completedPrRunnerMinutes / row.completedPrRuns
: null
}))
.sort((a, b) => b.runnerMinutes - a.runnerMinutes),
pools: [...pools.values()]
.map(({ waits, ...pool }) => ({ ...pool, queueP95Minutes: weightedPercentile(waits, 0.95) }))
.sort((a, b) => b.runnerMinutes - a.runnerMinutes)
}
}
export function sampleWorkflowRuns(runs, perStratum = 6, random = Math.random) {
const groups = new Map()
for (const run of runs) {
const key = `${workflowKey(run)}:${run.conclusion ?? run.status}`
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push(run)
}
return [...groups.values()].flatMap((group) => {
const shuffled = [...group]
for (let index = shuffled.length - 1; index > 0; index--) {
const target = Math.floor(random() * (index + 1))
;[shuffled[index], shuffled[target]] = [shuffled[target], shuffled[index]]
}
const selected = shuffled.slice(0, perStratum)
return selected.map((run) => ({ run, weight: group.length / selected.length }))
})
}
+95
View File
@@ -0,0 +1,95 @@
import { expect, it } from 'vitest'
import { runnerDemand, sampleWorkflowRuns } from './ci-runner-metrics.mjs'
import { collectRunnerDemand } from './ci-runner-demand.mjs'
const run = {
id: 1,
path: '.github/workflows/pr.yml',
event: 'pull_request',
conclusion: 'success'
}
const job = {
runner_name: 'hosted',
labels: ['ubuntu-latest'],
created_at: '2026-09-27T00:00:00Z',
started_at: '2026-09-27T00:03:00Z',
completed_at: '2026-09-27T00:08:00Z'
}
it('separates occupancy, queueing, cancellations and zero-job observations', () => {
const cancelled = { ...run, id: 2, conclusion: 'cancelled' }
const report = runnerDemand(
[run, cancelled],
[
{ run, weight: 2, jobs: [job, { ...job, runner_name: null }] },
{ run: cancelled, weight: 3, jobs: [job, { ...job, completed_at: null }] }
]
)
expect(report.workflows[0]).toMatchObject({
runnerMinutes: 25,
cancelledRunnerMinutes: 15,
runnerMinutesPerCompletedPrRun: 5
})
expect(report.pools[0].queueP95Minutes).toBe(3)
expect(report.incompleteJobs).toBe(1)
expect(runnerDemand([run], [{ run, weight: 1, jobs: [] }]).workflows[0].runnerMinutes).toBe(0)
})
it('weights each workflow/outcome stratum back to the full inventory', () => {
const runs = Array.from({ length: 20 }, (_, index) => ({
...run,
id: index,
conclusion: index < 10 ? 'success' : 'failure'
}))
const sample = sampleWorkflowRuns(runs, 2, () => 0.5)
expect(sample).toHaveLength(4)
expect(sample.reduce((sum, row) => sum + row.weight, 0)).toBe(20)
expect(new Set(sample.map((row) => row.run.id)).size).toBe(4)
})
it('paginates jobs and bounds run discovery to 24 complete hours', async () => {
const requests = []
const result = await collectRunnerDemand(
{ CI_METRICS_END: '2026-09-28T04:59:00Z' },
async (path) => {
requests.push(path)
if (path.includes('/jobs')) {
return {
total_count: 101,
jobs: path.includes('page=2') ? [job] : Array.from({ length: 100 }, () => job)
}
}
return {
total_count: requests.length === 1 ? 1 : 0,
workflow_runs: requests.length === 1 ? [run] : []
}
}
)
expect(result.report.start).toBe('2026-09-27T04:00:00.000Z')
expect(result.report.end).toBe('2026-09-28T04:00:00.000Z')
expect(requests.filter((path) => path.startsWith('actions/runs?'))).toHaveLength(24)
expect(result.samples[0].jobs).toHaveLength(101)
expect(result.report.workflows[0].runnerMinutes).toBe(505)
})
it('refuses to publish a silently truncated inventory', async () => {
await expect(
collectRunnerDemand({}, async () => ({ total_count: 1001, workflow_runs: [] }))
).rejects.toThrow('API limit')
})
it('groups ref-qualified paths under the stable workflow ID', () => {
const runs = [
{ ...run, id: 1, workflow_id: 42, path: '.github/workflows/pr.yml@main' },
{ ...run, id: 2, workflow_id: 42, path: '.github/workflows/pr.yml@feature' }
]
const sample = sampleWorkflowRuns(runs, 1, () => 0.5)
expect(sample).toHaveLength(1)
expect(sample[0].weight).toBe(2)
expect(
runnerDemand(
runs,
sample.map((row) => ({ ...row, jobs: [job] }))
).workflows
).toMatchObject([{ workflow: '.github/workflows/pr.yml', runs: 2, runnerMinutes: 10 }])
})
@@ -0,0 +1,84 @@
import { globSync, readFileSync } from 'node:fs'
import { posix, join } from 'node:path'
import ts from 'typescript-api'
const EXTENSIONS = [
'',
'.ts',
'.tsx',
'.mjs',
'.js',
'.cjs',
'.json',
'/index.ts',
'/index.tsx',
'/index.js'
]
const INDIRECT_INPUT =
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
function localPath(file, specifier) {
if (specifier.startsWith('.')) {
return posix.join(posix.dirname(file), specifier)
}
if (specifier.startsWith('@renderer/')) {
return `src/renderer/src/${specifier.slice(10)}`
}
if (specifier.startsWith('@/')) {
return `src/renderer/src/${specifier.slice(2)}`
}
return null
}
export function buildUnitDependencyGraph(sources) {
const reverse = new Map()
const opaque = new Set()
for (const [file, source] of sources) {
if (INDIRECT_INPUT.test(source) || file.startsWith('config/') || file.startsWith('tests/')) {
opaque.add(file)
}
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
const path = localPath(file, imported.fileName)
if (path === null) {
continue
}
const resolved = EXTENSIONS.map((extension) => path + extension).find((candidate) =>
sources.has(candidate)
)
if (!resolved) {
opaque.add(file)
continue
}
if (!reverse.has(resolved)) {
reverse.set(resolved, new Set())
}
reverse.get(resolved).add(file)
}
}
return { reverse, opaque }
}
export function collectUnitDependencyGraph(root = process.cwd()) {
const files = globSync(
[
'src/**/*.{ts,tsx,js,mjs,cjs,json}',
'config/**/*.{ts,tsx,js,mjs,cjs,json}',
'tests/**/*.{ts,tsx,js,mjs,cjs,json}'
],
{ cwd: root }
)
const sources = new Map(
files.map((file) => [file.replaceAll('\\', '/'), readFileSync(join(root, file), 'utf8')])
)
return { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
}
export function unitConsumers(seeds, reverse) {
const result = new Set(seeds)
for (const file of result) {
for (const consumer of reverse.get(file) ?? []) {
result.add(consumer)
}
}
return result
}
+41
View File
@@ -0,0 +1,41 @@
import { globSync } from 'node:fs'
import { defaultExclude } from 'vitest/config'
export const UNIT_INCLUDE = [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'config/scripts/**/*.test.ts',
'config/scripts/**/*.test.mjs',
'tests/tools/**/*.test.mjs',
'tests/e2e/**/*.unit.test.ts'
]
export const UNIT_EXCLUDE = [
...defaultExclude,
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts',
'src/main/daemon/shell-ready.test.ts',
'src/main/daemon/node-pty-fd-leak.test.ts',
'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts',
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
'src/main/pty/omp-shell-wrapper-alias-safety.test.ts',
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
'src/main/shell-startup-feature-channel.test.ts',
'src/main/terminal-history-fish-session.node-pty.test.ts',
'src/main/zsh-scoped-histfile.live-shell.test.ts',
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
'src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts',
'src/shared/fish-query-reply-child-stdin.node-pty.test.ts',
'src/shared/pty-reply-echo-shapes.node-pty.test.ts',
'src/shared/startup-shell-portability.live-shell.test.ts',
'src/shared/posix-command-path-lookup.test.ts',
'tests/e2e/relay-region-compatibility.unit.test.ts',
'tests/e2e/relay-region-correction.unit.test.ts',
'tests/e2e/cross-version-wire/**'
]
export function discoverUnitFiles(root = process.cwd()) {
return globSync(UNIT_INCLUDE, { cwd: root, exclude: UNIT_EXCLUDE })
.map((file) => file.replaceAll('\\', '/'))
.sort()
}
+60
View File
@@ -0,0 +1,60 @@
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { runProcessSync } from './script-child-process.mjs'
import { collectUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
import { discoverUnitFiles } from './ci-unit-files.mjs'
import { planUnitSelection } from './ci-unit-selection.mjs'
import { readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
export function prepareUnitPlan(env = process.env) {
const files = discoverUnitFiles()
let plan
try {
const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf8'))
if (env.GITHUB_EVENT_NAME !== 'pull_request') {
throw new Error('Full reference run')
}
const diff = runProcessSync({
program: 'git',
args: ['diff', '--name-only', '--no-renames', '-z', 'HEAD^1', 'HEAD'],
maxOutputBytes: 16 * 1024 * 1024
})
if (diff.code !== 0 || diff.timedOut) {
throw new Error('Changed paths unavailable')
}
plan = planUnitSelection({
files,
changed: diff.stdout.split('\0').filter(Boolean),
graph: collectUnitDependencyGraph(),
timings: readTimingBaseline('unit').timings,
event,
mode: env.ORCA_UNIT_SELECTION_MODE
})
} catch (error) {
plan = {
version: 1,
mode: 'shadow',
selectionAvailable: false,
reason: String(error),
files,
candidateFiles: files,
executionFiles: files,
shards: Array.from({ length: 8 }, (_, index) => ({ index: index + 1, count: 8 }))
}
}
writeAssignment('ci-shards/unit-selection.json', plan)
if (env.GITHUB_OUTPUT) {
appendFileSync(env.GITHUB_OUTPUT, `shards=${JSON.stringify(plan.shards)}\n`)
}
if (env.GITHUB_STEP_SUMMARY) {
appendFileSync(
env.GITHUB_STEP_SUMMARY,
`Unit selection: **${plan.mode}**; ${plan.candidateFiles.length}/${files.length} candidate files; ${plan.shards.length} execution shards. ${plan.reason}\n`
)
}
return plan
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
prepareUnitPlan()
}
+78
View File
@@ -0,0 +1,78 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect, it } from 'vitest'
import { runProcessSync } from './script-child-process.mjs'
it.each([true, false])(
'plans from a real Git diff, with parent evidence available: %s',
(withParent) => {
const root = mkdtempSync(join(tmpdir(), 'orca-unit-plan-'))
const git = (args) => {
const result = runProcessSync({ program: 'git', args, cwd: root })
expect(result.code, result.stderr).toBe(0)
return result.stdout.trim()
}
try {
mkdirSync(join(root, 'src'))
writeFileSync(join(root, 'src/value.ts'), 'export const value = 1')
writeFileSync(join(root, 'src/consumer.test.ts'), "import './value'")
writeFileSync(join(root, 'src/unrelated.test.ts'), 'export const unrelated = true')
git(['init', '--quiet'])
git(['add', 'src'])
const commit = [
'-c',
'user.name=CI Test',
'-c',
'user.email=ci-test@example.invalid',
'-c',
'commit.gpgsign=false',
'commit',
'--quiet',
'-m',
'fixture'
]
git(commit)
if (withParent) {
writeFileSync(join(root, 'src/value.ts'), 'export const value = 2')
git(['add', 'src'])
git(commit)
}
const sourceSha = git(['rev-parse', 'HEAD'])
const eventPath = join(root, 'event.json')
writeFileSync(eventPath, JSON.stringify({ pull_request: { draft: true } }))
const result = runProcessSync({
program: process.execPath,
args: [fileURLToPath(new URL('./ci-unit-plan.mjs', import.meta.url))],
cwd: root,
env: {
...process.env,
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_UNIT_SELECTION_MODE: 'selected',
GITHUB_EVENT_NAME: 'pull_request',
GITHUB_EVENT_PATH: eventPath,
GITHUB_SHA: sourceSha,
ORCA_SHARD_SOURCE_SHA: sourceSha,
GITHUB_OUTPUT: join(root, 'outputs'),
GITHUB_STEP_SUMMARY: join(root, 'summary')
}
})
expect(result.code, result.stderr).toBe(0)
const plan = JSON.parse(readFileSync(join(root, 'ci-shards/unit-selection.json'), 'utf8'))
expect(plan.sourceSha).toBe(sourceSha)
expect(plan.mode).toBe(withParent ? 'selected' : 'shadow')
expect(plan.executionFiles).toEqual(
withParent ? ['src/consumer.test.ts'] : ['src/consumer.test.ts', 'src/unrelated.test.ts']
)
expect(plan.reason).toBe(
withParent
? 'Transitive imports plus indirect-input consumers'
: 'Error: Changed paths unavailable'
)
expect(plan.shards).toHaveLength(withParent ? 1 : 8)
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
+113
View File
@@ -0,0 +1,113 @@
import { appendFileSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
export function reviewUnitSelection(records) {
const groups = new Map()
for (const { plan, timing } of records) {
const key = JSON.stringify([
timing.sourceSha,
timing.runId,
timing.runAttempt,
timing.nodeVersion
])
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ plan, timing })
}
return [...groups.values()].map((group) => {
const { plan, timing: first } = group[0]
const selected = new Set(plan.candidateFiles)
const files = new Set()
const shards = new Set()
const missedFailures = []
let invalid = false
let omittedMs = 0
let totalMs = 0
for (const { plan: other, timing } of group) {
invalid ||=
JSON.stringify(other) !== JSON.stringify(plan) ||
!first.sourceSha ||
plan.sourceSha !== first.sourceSha ||
timing.unhandledErrors !== 0 ||
!['passed', 'failed'].includes(timing.status) ||
timing.shard.count !== first.shard.count ||
shards.has(timing.shard.index) ||
timing.shard.index < 1 ||
timing.shard.index > first.shard.count
shards.add(timing.shard.index)
for (const [file, duration] of Object.entries(timing.timings)) {
invalid ||=
files.has(file) ||
!Number.isFinite(duration) ||
duration <= 0 ||
!['passed', 'failed', 'skipped'].includes(timing.results?.[file])
files.add(file)
totalMs += duration
if (!selected.has(file)) {
omittedMs += duration
if (timing.results?.[file] === 'failed') {
missedFailures.push(file)
}
}
}
}
const complete =
!invalid &&
shards.size === first.shard.count &&
JSON.stringify([...files].sort()) === JSON.stringify([...plan.files].sort())
return {
sourceSha: first.sourceSha,
runId: first.runId,
runAttempt: first.runAttempt,
nodeVersion: first.nodeVersion,
mode: plan.mode,
completeFullRun: complete && plan.mode === 'shadow',
selectionEvaluated: complete && plan.mode === 'shadow' && plan.selectionAvailable === true,
reason: plan.reason,
missedFailures,
files: files.size,
candidateFiles: selected.size,
measuredWorkerMs: totalMs,
potentiallyOmittedWorkerMs: omittedMs
}
})
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const directory = process.argv[2]
if (!directory) {
throw new Error('Usage: ci-unit-selection-review.mjs ARTIFACT_DIRECTORY')
}
const records = globSync('**/unit-timings.json', { cwd: directory }).map((file) => {
const timing = JSON.parse(readFileSync(join(directory, file), 'utf8'))
const plan = JSON.parse(
readFileSync(
join(directory, file.replace('unit-timings.json', 'unit-selection.json')),
'utf8'
)
)
return { timing, plan }
})
if (!records.length) {
throw new Error('No unit selection evidence')
}
const review = reviewUnitSelection(records)
writeFileSync(join(directory, 'selection-review.json'), `${JSON.stringify(review, null, 2)}\n`)
const summary = [
'## Unit selection evidence',
'',
...review.map(
(row) =>
`- Node ${row.nodeVersion}: ${row.completeFullRun ? 'complete full reference' : 'incomplete or selected evidence'}; ${row.selectionEvaluated ? 'selection evaluated' : 'not selection-validation evidence'}; ${row.candidateFiles}/${row.files} candidate files; ${row.missedFailures.length} failures outside selection; ${(row.potentiallyOmittedWorkerMs / 60_000).toFixed(1)} potentially omitted worker-minutes. ${row.reason ?? ''}`
),
'',
'Worker time overlaps across processes; it is not runner time. Promote only after representative complete references show no missed failures.',
''
].join('\n')
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
}
console.log(summary)
}
@@ -0,0 +1,55 @@
import { expect, it } from 'vitest'
import { reviewUnitSelection } from './ci-unit-selection-review.mjs'
const plan = {
sourceSha: 'sha',
mode: 'shadow',
selectionAvailable: true,
files: ['a', 'b'],
candidateFiles: ['a']
}
it('does not present full fallback runs as selection-validation evidence', () => {
const records = [record(1, 'a', 'passed'), record(2, 'b', 'passed')].map((row) => ({
...row,
plan: { ...plan, selectionAvailable: false }
}))
expect(reviewUnitSelection(records)[0]).toMatchObject({
completeFullRun: true,
selectionEvaluated: false
})
})
const record = (index, file, state) => ({
plan,
timing: {
sourceSha: 'sha',
runId: '1',
runAttempt: '1',
nodeVersion: '24',
status: state,
unhandledErrors: 0,
shard: { index, count: 2 },
timings: { [file]: 100 },
results: { [file]: state }
}
})
it('finds omitted failures in a complete failing reference run', () => {
expect(
reviewUnitSelection([record(1, 'a', 'passed'), record(2, 'b', 'failed')])[0]
).toMatchObject({ completeFullRun: true, missedFailures: ['b'], potentiallyOmittedWorkerMs: 100 })
})
it('does not call incomplete, duplicate, interrupted or selected evidence a full reference', () => {
const a = record(1, 'a', 'passed'),
b = record(2, 'b', 'passed')
for (const rows of [
[a],
[a, a],
[a, { ...b, timing: { ...b.timing, unhandledErrors: 1 } }],
[a, { ...b, timing: { ...b.timing, status: 'interrupted' } }],
[a, b].map((row) => ({ ...row, plan: { ...plan, mode: 'selected' } }))
]) {
expect(reviewUnitSelection(rows).every((row) => !row.completeFullRun)).toBe(true)
}
})
+62
View File
@@ -0,0 +1,62 @@
import { unitConsumers } from './ci-unit-dependency-graph.mjs'
import { balanceFiles } from './ci-shard-assignment.mjs'
export function selectUnitFiles(files, changed, graph) {
const full = (reason) => ({ files, reason, full: true })
if (!changed.length) {
return full('Missing changed-path evidence')
}
if (changed.some((file) => !file.startsWith('src/') || !graph.files.has(file))) {
return full('Global, deleted, renamed or unknown input')
}
for (const file of changed) {
const consumers = unitConsumers([file], graph.reverse)
if (!files.some((test) => consumers.has(test))) {
return full('No proven test coverage for changed inputs')
}
}
const affected = unitConsumers([...changed, ...graph.opaque], graph.reverse)
const selected = files.filter((file) => affected.has(file))
if (!selected.length) {
return full('No proven test coverage for changed inputs')
}
return {
files: selected,
reason: 'Transitive imports plus indirect-input consumers',
full: false
}
}
export function planUnitSelection({ files, changed, graph, timings, event, mode = 'shadow' }) {
const candidate = selectUnitFiles(files, changed, graph)
const selected = mode === 'selected' && event?.pull_request?.draft === true && !candidate.full
const executionFiles = selected ? candidate.files : files
const totalMs = balanceFiles(executionFiles, 1, timings).shards[0].durationMs
const count = selected
? Math.max(1, Math.min(8, Math.ceil(totalMs / 900_000), executionFiles.length))
: 8
return {
version: 1,
mode: selected ? 'selected' : 'shadow',
selectionAvailable: !candidate.full,
reason: candidate.reason,
files,
candidateFiles: candidate.files,
executionFiles,
shards: Array.from({ length: count }, (_, index) => ({ index: index + 1, count }))
}
}
export function auditUnitSelection(plan, results) {
const candidates = new Set(plan.candidateFiles)
const omittedFailures = Object.entries(results)
.filter(([file, state]) => state === 'failed' && !candidates.has(file))
.map(([file]) => file)
return {
mode: plan.mode,
discovered: plan.files.length,
candidate: candidates.size,
executed: Object.keys(results).length,
omittedFailures
}
}
+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest'
import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
import { auditUnitSelection, planUnitSelection, selectUnitFiles } from './ci-unit-selection.mjs'
const sources = new Map(
Object.entries({
'src/leaf.ts': 'export const value = 1',
'src/forward.ts': `export * from './leaf'`,
'src/consumer.test.ts': `import './forward'`,
'src/dynamic.test.ts': `import('./leaf')`,
'src/require.test.ts': `require('./leaf')`,
'src/scan.test.ts': `import { readFileSync } from 'node:fs'; readFileSync('src/leaf.ts')`,
'src/indirect.ts': `import(pathFromSettings)`,
'src/indirect.test.ts': `import './indirect'`,
'src/unrelated.test.ts': 'export const test = 1',
'src/renderer/src/view.tsx': 'export const value = 1',
'src/view.test.ts': `import '@renderer/view'; import '@/view'`
})
)
const graph = { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
const files = [...sources.keys()].filter((file) => file.endsWith('.test.ts')).sort()
describe('conservative unit selection', () => {
it('follows re-exports, literal dynamic imports and requires, retaining indirect readers', () => {
expect(selectUnitFiles(files, ['src/leaf.ts'], graph).files).toEqual([
'src/consumer.test.ts',
'src/dynamic.test.ts',
'src/indirect.test.ts',
'src/require.test.ts',
'src/scan.test.ts'
])
})
it('resolves renderer aliases and changed tests without executing their source', () => {
expect(selectUnitFiles(files, ['src/renderer/src/view.tsx'], graph).files).toContain(
'src/view.test.ts'
)
expect(selectUnitFiles(files, ['src/unrelated.test.ts'], graph).files).toContain(
'src/unrelated.test.ts'
)
})
it('does not mistake unrelated opaque readers for coverage of a new entry point', () => {
const uncovered = { ...graph, files: new Set([...graph.files, 'src/entry.ts']) }
expect(selectUnitFiles(files, ['src/entry.ts'], uncovered)).toMatchObject({ full: true, files })
})
it.each(
[
[],
['src/deleted.ts'],
['src/deleted.ts', 'src/leaf.ts'],
['pnpm-lock.yaml'],
['config/vitest.config.ts']
].map((changed) => ({ changed }))
)('runs everything for incomplete/global evidence: $changed', ({ changed }) => {
expect(selectUnitFiles(files, changed, graph)).toMatchObject({ files, full: true })
})
it('keeps full coverage by default and on every non-draft commit', () => {
const base = { files, changed: ['src/leaf.ts'], graph, timings: {} }
for (const event of [
{},
{ pull_request: { draft: false } },
{ pull_request: { draft: true } }
]) {
expect(planUnitSelection({ ...base, event }).executionFiles).toEqual(files)
}
expect(
planUnitSelection({ ...base, mode: 'selected', event: { pull_request: { draft: false } } })
.executionFiles
).toEqual(files)
const selected = planUnitSelection({
...base,
mode: 'selected',
event: { pull_request: { draft: true } }
})
expect(selected.executionFiles).not.toContain('src/unrelated.test.ts')
expect(selected.shards).toEqual([{ index: 1, count: 1 }])
})
it('records failures that would have been missed while shadow runs remain full', () => {
const plan = planUnitSelection({ files, changed: ['src/leaf.ts'], graph, timings: {} })
expect(
auditUnitSelection(plan, {
'src/consumer.test.ts': 'failed',
'src/unrelated.test.ts': 'failed',
'src/view.test.ts': 'passed'
}).omittedFailures
).toEqual(['src/unrelated.test.ts'])
})
})
+37 -2
View File
@@ -1,4 +1,5 @@
import { relative } from 'node:path'
import { readFileSync } from 'node:fs'
import { BaseSequencer } from 'vitest/node'
import { balanceFiles, readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
@@ -6,12 +7,46 @@ export default class TimingSequencer extends BaseSequencer {
async shard(specs) {
const { index, count } = this.ctx.config.shard
const key = (spec) => relative(this.ctx.config.root, spec.moduleId).replaceAll('\\', '/')
let execution = specs
let selectionReason = 'Full suite: no verified selection plan'
try {
const plan = JSON.parse(
readFileSync(
process.env.ORCA_UNIT_SELECTION_PLAN ?? 'ci-shards/unit-selection.json',
'utf8'
)
)
if (
plan.version !== 1 ||
!plan.sourceSha ||
plan.sourceSha !== process.env.ORCA_SHARD_SOURCE_SHA ||
JSON.stringify([...plan.files].sort()) !== JSON.stringify(specs.map(key).sort()) ||
!Array.isArray(plan.executionFiles) ||
plan.executionFiles.some((file) => !plan.files.includes(file))
) {
throw new Error('Selection provenance or discovery differs')
}
const allowed = new Set(plan.executionFiles)
if (allowed.size === 0) {
throw new Error('Empty execution selection')
}
execution = specs.filter((spec) => allowed.has(key(spec)))
selectionReason = plan.reason
} catch (error) {
console.log(`Running every discovered unit test: ${error.message}`)
}
const baseline = readTimingBaseline('unit')
const assignment = balanceFiles(specs.map(key), count, baseline.timings, baseline.overheadMs)
const assignment = balanceFiles(
execution.map(key),
count,
baseline.timings,
baseline.overheadMs
)
writeAssignment(process.env.ORCA_SHARD_MANIFEST ?? 'ci-shards/unit-assignment.json', {
...assignment,
baselineSha256: baseline.baselineSha256,
selectedShard: index
selectedShard: index,
selectionReason
})
const selected = new Set(assignment.shards[index - 1].files)
return specs.filter((spec) => selected.has(key(spec)))
+44
View File
@@ -0,0 +1,44 @@
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import TimingSequencer from './ci-unit-sequencer.mjs'
let root
afterEach(() => {
vi.unstubAllEnvs()
if (root) {
rmSync(root, { recursive: true, force: true })
}
})
it.each(['valid', 'stale', 'missing-file', 'missing-artifact'])(
'preserves complete shard coverage with %s planning evidence',
async (kind) => {
root = mkdtempSync(join(tmpdir(), 'unit-sequencer-'))
const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts']
const plan = {
version: 1,
sourceSha: kind === 'stale' ? 'old' : 'current',
files: kind === 'missing-file' ? files.slice(1) : files,
executionFiles: files.slice(0, 2)
}
const planPath = join(root, 'selection.json')
if (kind !== 'missing-artifact') {
writeFileSync(planPath, JSON.stringify(plan))
}
vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath)
vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current')
vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json'))
const assigned = []
for (const index of [1, 2]) {
const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } })
const specs = files.map((file) => ({ moduleId: join(root, file) }))
assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId))
}
expect(assigned.sort()).toEqual(
(kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort()
)
expect(new Set(assigned).size).toBe(assigned.length)
}
)
@@ -1,5 +1,7 @@
import { relative } from 'node:path'
import { appendFileSync, readFileSync } from 'node:fs'
import { writeAssignment } from './ci-shard-assignment.mjs'
import { auditUnitSelection } from './ci-unit-selection.mjs'
export function moduleDuration(diagnostic) {
return Math.max(
@@ -20,12 +22,19 @@ export default class UnitTimingReporter {
}
onTestRunEnd(modules, errors, reason) {
const results = Object.fromEntries(
modules.map((module) => [
relative(this.ctx.config.root, module.moduleId).replaceAll('\\', '/'),
module.state?.() ?? 'unknown'
])
)
writeAssignment(process.env.ORCA_UNIT_TIMING_REPORT ?? 'ci-shards/unit-timings.json', {
metric: 'module-duration-v1',
nodeVersion: process.versions.node,
shard: this.ctx.config.shard ?? { index: 1, count: 1 },
status: reason,
unhandledErrors: errors.length,
results,
timings: Object.fromEntries(
modules.map((module) => [
relative(this.ctx.config.root, module.moduleId).replaceAll('\\', '/'),
@@ -33,5 +42,23 @@ export default class UnitTimingReporter {
])
)
})
try {
const plan = JSON.parse(
readFileSync(
process.env.ORCA_UNIT_SELECTION_PLAN ?? 'ci-shards/unit-selection.json',
'utf8'
)
)
const audit = auditUnitSelection(plan, results)
writeAssignment('ci-shards/unit-selection-audit.json', audit)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(
process.env.GITHUB_STEP_SUMMARY,
`Unit selection (${audit.mode}): ${audit.candidate}/${audit.discovered} candidate files; ${audit.omittedFailures.length} failures outside selection.\n`
)
}
} catch {
// Timing evidence remains useful when selection planning was unavailable.
}
}
}
+10 -6
View File
@@ -7,10 +7,12 @@ import { describe, expect, it } from 'vitest'
* around `gtk_init()` while FontConfig warmed up on a pool thread, and below
* glibc 2.41 that frees `environ` under a concurrent `getenv()` — a launch-time
* use-after-free on every Ubuntu we support (stablyai/orca#20081). 43.7.0 stops
* freeing the published `environ`. A downgrade past it re-ships that crash, and
* nothing else in the tree would notice.
* freeing the published `environ`. 43.7.0 itself threw `Invalid guestInstanceId` when a
* loaded `<webview>` left the DOM, which blanked browser tabs React re-inserted
* (electron/electron#53989); 43.7.4 fixes it. A downgrade past either re-ships the bug,
* and nothing else in the tree would notice.
*/
const MINIMUM_ELECTRON_VERSION = '43.7.0'
const MINIMUM_ELECTRON_VERSION = '43.7.4'
function parseVersion(specifier: string): [number, number, number] {
const match = /(\d+)\.(\d+)\.(\d+)/.exec(specifier)
@@ -35,15 +37,17 @@ describe('electron runtime floor', () => {
it.each([
['42.9.0', false],
['43.6.0', false],
['43.7.0', true],
['43.7.1', true],
['43.7.0', false],
['43.7.3', false],
['43.7.4', true],
['43.7.5', true],
['43.8.0', true],
['44.0.0', true]
])('reads %s as meeting the floor: %s', (specifier, expected) => {
expect(meetsRuntimeFloor(specifier)).toBe(expected)
})
it('pins Electron at or above the glibc environ-race fix', () => {
it('pins Electron at or above the glibc environ-race and webview-detach fixes', () => {
const packageJson = JSON.parse(
readFileSync(join(__dirname, '../../package.json'), 'utf-8')
) as { devDependencies: Record<string, string> }
@@ -1,5 +1,6 @@
import { execFileSync } from 'node:child_process'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
export function selectPullRequestDiffBase(requestedBase, headParents, eventName) {
if (eventName === 'pull_request' && headParents.length >= 2) {
@@ -21,3 +22,15 @@ export function resolvePullRequestDiffBase(
.split(/\s+/)
return selectPullRequestDiffBase(requestedBase, headParents, eventName)
}
// Why a CLI: the workflow's inline gates used `git diff --merge-base "$BASE_SHA" "$HEAD_SHA"`,
// which needs the payload base to be reachable and so forced a full-history checkout. Printing
// the resolved base lets those steps diff against `HEAD^1` with the same logic the Node gates
// already use, instead of each one open-coding it.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const resolved = resolvePullRequestDiffBase(process.cwd(), process.argv[2])
if (!resolved) {
throw new Error('No diff base: pass the pull request base SHA, or run on a merge ref.')
}
process.stdout.write(`${resolved}\n`)
}
@@ -38,6 +38,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([
'Claude Agent Teams',
'Cline',
'Codebuff',
'Freebuff',
'Codex',
'Command Code',
'Cursor',
@@ -43,12 +43,13 @@ it('preserves deleted and renamed inputs and falls back to extraction on detecti
(candidate) => candidate.name === 'Verify localization extraction'
)
expect(step.env).toEqual({
BASE_SHA: '${{ github.event.pull_request.base.sha }}',
HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
})
expect(step.run).toContain(
'git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA"'
)
// The base side comes from the merge ref's first parent, so the gate needs no merge base and
// works on a shallow checkout. The payload head SHA is no longer read: HEAD is the merged tree
// the gate is actually deciding about.
expect(step.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
expect(step.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD')
expect(step.run).not.toContain('--diff-filter')
expect(step.run).toContain('&& [ "$scope" = false ]; then')
expect(step.run).toMatch(/else\s+pnpm run verify:localization-extraction\s+fi/)
@@ -120,6 +120,8 @@ const BUNDLER_CACHE_PATHS = ['metro-cache', '.expo', 'node_modules/.cache']
const REVIEWED_COMPUTED_PATHS = [
'${{ steps.electron-package-cache.outputs.cache-root }}',
'${{ steps.pnpm-store.outputs.path }}',
// Only pnpm's lockfile-verified.jsonl record, never Metro transforms.
'${{ steps.verification-cache.outputs.path }}',
"${{ github.event_name != 'pull_request' && 'pnpm' || '' }} store"
]
@@ -9,6 +9,7 @@ import {
createBundleServer,
installShellDouble,
readBridgeFaultGrant,
readBridgePagePainted,
readBridgeProtocolVersion,
readShellCsp
} from './mobile-web-app-render-harness.mjs'
@@ -110,6 +111,7 @@ let routeChunks = {}
let cspHeader = null
let bridgeVersion = null
let faultGrant = null
let paintName = null
beforeAll(async () => {
if (!bundles) {
@@ -118,6 +120,7 @@ beforeAll(async () => {
cspHeader = await readShellCsp()
bridgeVersion = await readBridgeProtocolVersion()
faultGrant = await readBridgeFaultGrant()
paintName = await readBridgePagePainted()
scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-app-agent-history-'))
const built = await buildMobileWebAppBundle({ outDir: join(scratch, 'bundle') })
routeChunks = built.routeChunks
@@ -227,9 +230,11 @@ async function openRoute(route, awaitText, options = {}) {
return opened
}
/** Every grant-gated notify the page posted, whole and in order, as the shell received them. */
function readNotifies(page) {
return page.evaluate(() => globalThis.__orcaRenderCheckNotifies ?? [])
/** Every grant-gated notify the page posted, whole and in order, as the shell received them. The
* paint report is not one: every page posts it once its route screen has a frame. */
async function readNotifies(page) {
const notifies = await page.evaluate(() => globalThis.__orcaRenderCheckNotifies ?? [])
return notifies.filter((frame) => frame.name !== paintName)
}
describeRender('the agent-history route in a real browser', () => {
@@ -302,9 +302,6 @@ describeDrawer('the device Back key reaching a sheet on the page', () => {
host: SHELL_HOST,
storage: {},
faultGrant,
// What a shell that knows the lane offers, and the frame it would send. Without the first
// the page posts no claim at all, which is the older-shell arm the unit suites pin.
accepts: [backNames.claim],
backFrame: backNames.frame
})
await page.goto(`${origin}/`, { waitUntil: 'load' })
@@ -164,9 +164,9 @@ export async function readBrowserFrameQuality() {
}
/**
* The page's client-identity placeholder and the `init.accepts` name that unlocks it, read from
* the module that declares both. A rig carrying its own copy would go on passing after the real
* pair moved, which is the whole reason every other constant here is read rather than retyped.
* The page's client-identity placeholder, read from the module that declares it. A rig carrying its
* own copy would go on passing after the real one moved, which is the whole reason every other
* constant here is read rather than retyped.
*/
export async function readBridgePageClientIdentity() {
const source = await readFile(
@@ -180,10 +180,7 @@ export async function readBridgePageClientIdentity() {
}
return match[1]
}
return {
placeholder: read('BRIDGE_PAGE_CLIENT_ID'),
accept: read('BRIDGE_PAGE_CLIENT_IDENTITY_ACCEPT')
}
return { placeholder: read('BRIDGE_PAGE_CLIENT_ID') }
}
/** The grant the shell offers every page, read from the same source for the same reason. */
@@ -264,7 +261,6 @@ export function installShellDouble({
grants,
pageRoutes = null,
pageRouteGrants = null,
accepts = null,
backFrame = null,
replies,
streams = [],
@@ -323,9 +319,6 @@ export function installShellDouble({
// Omitted when the caller names none, which is the older-shell case the page falls back
// on: an absent field is not an empty one, and the page reads the difference.
...(pageRouteGrants === null ? {} : { pageRouteGrants }),
// Omitted when a check names none, which is the shell that performs no swap and the
// state every other rig in this directory runs in.
...(accepts === null ? {} : { accepts }),
// Omitted for a shell too old to name one, which is the case the page has a panel for.
...(route === null ? {} : { route }),
...(host === null ? {} : { host }),
+3 -10
View File
@@ -135,8 +135,7 @@ async function openPage({
shellHost = SHELL_HOST,
shellStorage = {},
shellGrants,
shellPageRoutes = null,
shellAccepts = null
shellPageRoutes = null
} = {}) {
const page = await browser.newPage({ viewport: { width: 390, height: 844 } })
if (shellRoute !== undefined) {
@@ -151,8 +150,7 @@ async function openPage({
storage: shellStorage,
faultGrant,
grants: shellGrants ?? [faultGrant],
pageRoutes: shellPageRoutes,
accepts: shellAccepts
pageRoutes: shellPageRoutes
})
}
const errors = []
@@ -587,11 +585,7 @@ describeRender('the Route A page in a real browser', () => {
})
)
try {
// The one case that advertises the report, because it is the only one asserting on it.
const opened = await openPage({
shellRoute: { pathname: HOST_ROUTE },
shellAccepts: [paintName]
})
const opened = await openPage({ shellRoute: { pathname: HOST_ROUTE } })
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
await opened.page.waitForFunction(
() => document.documentElement.dataset.orcaWebEntry === 'mounted',
@@ -639,7 +633,6 @@ describeRender('the Route A page in a real browser', () => {
const route = `${HOST_ROUTE}/pr/render-check-tree`
const opened = await openPage({
shellRoute: { pathname: route },
shellAccepts: [paintName],
shellPageRoutes: [HOST_ROUTE_PATTERN, '/h/[hostId]/pr/[worktreeId]']
})
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
@@ -148,8 +148,6 @@ async function openSessionWithTerminalTab() {
faultGrant,
grants: [faultGrant, ...sessionGrants()],
pageRoutes: PAGE_ROUTE_PATTERNS,
// The page claims an identity only against a shell that says it swaps one in.
accepts: [pageClientIdentity.accept],
streams: ['session.tabs.subscribe', 'terminal.subscribe'],
replies: {
'worktree.show': { worktree: { id: WORKTREE, name: WORKTREE, path: `/tmp/${WORKTREE}` } },
@@ -57,6 +57,8 @@ import { TerminalWebView } from ${JSON.stringify(componentPath)}
export default function TerminalProbeRoute() {
const handleRef = useRef(null)
// The terminal frame as React Native laid it out, which is what the session fits.
const frameRef = useRef({ width: 0, height: 0 })
const [mounted, setMounted] = useState(true)
const onSelectionCopy = useCallback((text) => {
globalThis.__orcaTerminalCopied = text
@@ -71,10 +73,11 @@ export default function TerminalProbeRoute() {
globalThis.__orcaTerminalEngineErrors = globalThis.__orcaTerminalEngineErrors ?? []
globalThis.__orcaTerminalBeforeInput = []
globalThis.__orcaTerminalProbe = {
init: (cols, rows, data) => handleRef.current?.init(cols, rows, data, false, []),
init: (cols, rows, initialData) =>
handleRef.current?.init({ cols, rows, initialData, oscLinks: [], frame: frameRef.current }),
write: (data) => handleRef.current?.write(data),
selectAll: () => handleRef.current?.doSelectAll(),
measure: () => handleRef.current?.measureFitDimensions(),
fit: () => handleRef.current?.fitDimensions(frameRef.current),
awaitReady: () => handleRef.current?.awaitReady(),
setMounted: (next) => setMounted(next)
}
@@ -90,14 +93,21 @@ export default function TerminalProbeRoute() {
}, [])
return (
<View testID="terminal-probe" style={{ flex: 1 }}>
{mounted ? (
<TerminalWebView
ref={handleRef}
onWebReady={onWebReady}
onEngineError={onEngineError}
onSelectionCopy={onSelectionCopy}
/>
) : null}
<View
style={{ flex: 1 }}
onLayout={(event) => {
frameRef.current = event.nativeEvent.layout
}}
>
{mounted ? (
<TerminalWebView
ref={handleRef}
onWebReady={onWebReady}
onEngineError={onEngineError}
onSelectionCopy={onSelectionCopy}
/>
) : null}
</View>
{/* The shape the terminal's live input takes on the page: xterm's own textarea is inert by
the document's design, so this is where typed text arrives. */}
<TextInput testID="terminal-live-input" style={{ fontSize: 16 }} />
@@ -480,6 +480,14 @@ describeRender(
})
await openProbeTerminal(page)
expect(documentChunk, 'the document was served as its own chunk').not.toBe(null)
// Ready is notified with the replay's fit still a frame away. Left pending, that fit
// commits the resized box below, the resize refit then has nothing to do, and there is no
// frame to hold; so the document settles first. Since the terminal is built before
// ready, the fixture returns fast enough to land inside that frame.
await page.evaluate(
() =>
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
)
await page.evaluate((chunk) => {
const state = globalThis.__orcaScheduler
@@ -627,14 +635,14 @@ describeRender(
await page.close()
}, 300_000)
it('measures a fit through the handle and records what beforeinput reports', async () => {
it('fits through the handle from the ready box and records what beforeinput reports', async () => {
const { page } = await openTerminal()
await openProbeTerminal(page)
// The handle's own round trip: a measure is a command in and a notify back, and on the page
// both halves are direct calls rather than a bridge. Null would mean the document answered
// nothing, or answered a grid too small to fit.
const fit = await page.evaluate(() => globalThis.__orcaTerminalProbe.measure())
// The fit is the app's own, from the cell box the document put in web-ready, against the
// frame React Native laid out, as the session's is. Null would mean the ready carried no box,
// or a grid too small to fit.
const fit = await page.evaluate(() => globalThis.__orcaTerminalProbe.fit())
expect(fit).not.toBeNull()
expect(fit.cols).toBeGreaterThanOrEqual(20)
expect(fit.rows).toBeGreaterThanOrEqual(8)
+10 -4
View File
@@ -68,12 +68,10 @@ export function hashedAsset(bytes, extension) {
}
}
function readIntegerConstant(source, name) {
function readIntegerConstant(source, name, file = 'src/shared/protocol-version.ts') {
const match = new RegExp(`export const ${name} = (\\d+)`).exec(source)
if (!match) {
throw new Error(
`[mobile-web-bundle-manifest] ${name} not found in src/shared/protocol-version.ts`
)
throw new Error(`[mobile-web-bundle-manifest] ${name} not found in ${file}`)
}
return Number.parseInt(match[1], 10)
}
@@ -94,6 +92,13 @@ export async function readProtocolWindow() {
}
}
/** The page's build number, parsed for the same reason the protocol window is. */
export async function readPageVersion() {
const file = join('src', 'shared', 'mobile-web-bundle', 'manifest-contract.ts')
const source = await readFile(join(projectDir, file), 'utf8')
return readIntegerConstant(source, 'MOBILE_WEB_PAGE_VERSION', file)
}
export async function readDesktopVersion() {
const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'))
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
@@ -124,6 +129,7 @@ export async function writeMobileWebBundleTree({
desktopVersion,
minCompatibleRuntimeProtocolVersion: protocolWindow.minCompatibleRuntimeProtocolVersion,
runtimeProtocolVersion: protocolWindow.runtimeProtocolVersion,
pageVersion: await readPageVersion(),
entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT,
totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0),
assets,
@@ -1,10 +1,16 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
computeMobileWebBundleBuildId,
contentTypeForExtension,
hashedAsset,
serializeMobileWebBundleAssets
serializeMobileWebBundleAssets,
sha256Hex,
writeMobileWebBundleTree
} from './mobile-web-bundle-manifest.mjs'
import { MobileWebBundleManifestSchema } from '../../src/shared/mobile-web-bundle/manifest-contract.js'
describe('computeMobileWebBundleBuildId', () => {
const assets = [
@@ -55,3 +61,30 @@ describe('hashedAsset', () => {
expect(() => hashedAsset(Buffer.from('body'), 'wasm')).toThrow(/no content type registered/)
})
})
describe('writeMobileWebBundleTree', () => {
it('stamps the page version the contract names, which the shell floors on', async () => {
const outDir = await mkdtemp(join(tmpdir(), 'mobile-web-bundle-manifest-'))
try {
const bytes = Buffer.from('<!doctype html>\n', 'utf8')
const entry = {
path: 'index.html',
bytes,
sha256: sha256Hex(bytes),
byteLength: bytes.byteLength,
contentType: contentTypeForExtension('html')
}
await writeMobileWebBundleTree({
outDir,
written: [entry],
desktopVersion: '1.0.0',
protocolWindow: { runtimeProtocolVersion: 3, minCompatibleRuntimeProtocolVersion: 3 }
})
const manifest = JSON.parse(await readFile(join(outDir, 'manifest.json'), 'utf8'))
expect(Number.isInteger(manifest.pageVersion)).toBe(true)
expect(MobileWebBundleManifestSchema.parse(manifest).pageVersion).toBe(manifest.pageVersion)
} finally {
await rm(outDir, { recursive: true, force: true })
}
})
})
@@ -97,12 +97,12 @@ it('only skips the unchanged smoke after a successful diff and dependency analys
)
expect(step.env).toEqual({
BASE_SHA: '${{ github.event.pull_request.base.sha }}',
HEAD_SHA: '${{ github.event.pull_request.head.sha }}',
ORCA_BACKGROUND_LAUNCH: '1'
})
expect(step.run).toContain(
'git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA"'
)
// See the localization gate: HEAD^1 removes the merge-base computation, so a shallow checkout
// is enough and the payload head SHA is unused.
expect(step.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
expect(step.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD')
expect(step.run).not.toContain('--diff-filter')
expect(step.run).toContain('&& [ "$scope" = false ]; then')
expect(step.run).toMatch(/else\s+pnpm run smoke:orcad-terminal\s+fi/)
+1 -1
View File
@@ -24,7 +24,7 @@ export function linuxFormatArguments({ format, appDirectory, outputDirectory })
}
return [
'--config',
'config/electron-builder.config.cjs',
'config/electron-builder-pr-linux.config.cjs',
'--linux',
format,
'--x64',
+16 -1
View File
@@ -11,6 +11,7 @@ import {
symlinkSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
@@ -55,7 +56,7 @@ it('preserves configured hooks, architecture, names, and PR compression with exa
})
expect(args).toEqual([
'--config',
'config/electron-builder.config.cjs',
'config/electron-builder-pr-linux.config.cjs',
'--linux',
'deb',
'--x64',
@@ -209,3 +210,17 @@ describe('independent Linux package formats', () => {
expect(readdirSync(outputDirectory)).toEqual([])
})
})
it('changes only CI deb/rpm compression levels without mutating release settings or hooks', () => {
const require = createRequire(import.meta.url)
const release = require('../electron-builder.config.cjs')
const before = { deb: { ...release.deb }, rpm: { ...release.rpm } }
const ci = require('../electron-builder-pr-linux.config.cjs')
expect(release.deb).toEqual(before.deb)
expect(release.rpm).toEqual(before.rpm)
expect(ci).toEqual({
...release,
deb: { ...release.deb, fpm: [...(release.deb.fpm ?? []), '--deb-compression-level=1'] },
rpm: { ...release.rpm, fpm: [...(release.rpm.fpm ?? []), '--rpm-compression-level=1'] }
})
})
+2
View File
@@ -152,6 +152,8 @@ const CROSS_VERSION_WIRE_PREFIXES = [
'src/main/native-chat/agent-session-journal/',
'src/main/native-chat/agent-session-wire/',
'src/main/runtime/agent-session-record-store',
'src/main/runtime/agent-session-recovery-capsule',
'src/shared/agent-session-resume-marker',
'src/main/runtime/rpc/dispatcher',
'src/main/runtime/rpc/methods/agent-launch',
'src/main/runtime/rpc/methods/ai-vault.ts',
+16 -3
View File
@@ -530,7 +530,11 @@ describe('PR Checks skip wiring', () => {
)
expect(classify.run).toContain('--diff-filter=ACDMR')
expect(classify.run).toContain('--no-renames')
expect(classify.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
// HEAD is the merge commit, so HEAD^1 is the base side and no merge base is computed.
// That is what lets this job check out shallowly, which every other job waits on.
expect(classify.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
expect(classify.run).toContain('"$DIFF_BASE" HEAD')
expect(classify.run).not.toContain('--merge-base "$')
expect(classify.run).toContain('node config/scripts/pr-code-change-scope.mjs')
expect(classify.run).toContain('tee -a "$GITHUB_OUTPUT"')
expect(prWorkflow.jobs.code_paths.outputs.should_run).toBe(
@@ -582,12 +586,21 @@ describe('PR Checks skip wiring', () => {
it('gates each expensive job on its classifier and cache prerequisite', () => {
for (const jobName of expensiveJobs.filter((jobName) => jobName !== 'test')) {
expect(prWorkflow.jobs[jobName].needs, jobName).toEqual(['code_paths'])
expect(prWorkflow.jobs[jobName].needs, jobName).toEqual(
['package', 'package_windows'].includes(jobName)
? ['code_paths', 'static_analysis', 'typecheck']
: ['code_paths']
)
expect(prWorkflow.jobs[jobName].if, jobName).toBe(
`needs.code_paths.outputs.${jobName} == 'true'`
)
}
expect(prWorkflow.jobs.test.needs).toEqual(['code_paths', 'test_native_cache'])
expect(prWorkflow.jobs.test.needs).toEqual([
'code_paths',
'test_native_cache',
'static_analysis',
'typecheck'
])
expect(prWorkflow.jobs.test.if).toContain("needs.code_paths.outputs.test == 'true'")
expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'success'")
expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'skipped'")
+2 -2
View File
@@ -238,7 +238,7 @@ describe('PR E2E gate contract', () => {
})
it('scopes detection to the PR range so base drift cannot false-trigger', () => {
expect(filterStep.run).toContain('--merge-base "$BASE" "$HEAD"')
expect(filterStep.run).toMatch(/diff-base\.mjs "\$BASE"[\s\S]*"\$DIFF_BASE" HEAD/)
expect(filterStep.run).toContain('set -euo pipefail')
})
@@ -444,7 +444,7 @@ describe('PR E2E gate contract', () => {
})
it('scopes the VM rollback oracle to the PR range and recipe schema authorities', () => {
expect(rollbackStep.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
expect(rollbackStep.run).toMatch(/diff-base\.mjs "\$BASE_SHA"[\s\S]*"\$DIFF_BASE" HEAD --/)
expect(rollbackStep.run).toContain('src/shared/ephemeral-vm-recipes.ts')
expect(rollbackStep.run).toContain('src/shared/orca-yaml-hook-types.ts')
expect(selectPrE2eSpecs(['src/shared/ephemeral-vm-recipes.ts'])).toEqual([
+2 -2
View File
@@ -1,8 +1,8 @@
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
export function prCheckRunTitle({ number, sourceSha, workflowSha }) {
return `PR ${number} | source ${sourceSha} | workflow ${workflowSha}`
export function prCheckRunTitle({ number, sourceSha, workflowSha, unitMode = 'full' }) {
return `PR ${number} | source ${sourceSha} | workflow ${workflowSha} | unit ${unitMode}`
}
export function reusablePrCheckRun(runs, identity) {
+7 -1
View File
@@ -45,6 +45,12 @@ describe('ready-for-review required check reuse', () => {
it('reuses a completed success only for the identical PR, merge source and workflow', () => {
expect(reusablePrCheckRun([passed], identity)).toBe(passed)
expect(reusablePrCheckRun([], identity)).toBeUndefined()
expect(
reusablePrCheckRun(
[{ ...passed, display_title: prCheckRunTitle({ ...identity, unitMode: 'selected' }) }],
identity
)
).toBeUndefined()
for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha', 'runId']) {
const changed = key === 'number' ? 43 : key === 'runId' ? '123' : 'd'.repeat(40)
expect(reusablePrCheckRun([passed], { ...identity, [key]: changed }), key).toBeUndefined()
@@ -121,7 +127,7 @@ describe('ready-for-review required check reuse', () => {
it('keeps required skips conditional on proof and leaves advisory routing eligible', () => {
expect(workflow['run-name']).toBe(
'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
"PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}"
)
expect(workflow.on.pull_request.types).toContain('ready_for_review')
const detector = workflow.jobs.code_paths
@@ -1,6 +1,7 @@
import { existsSync, globSync, readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
import { UNIT_EXCLUDE } from './ci-unit-files.mjs'
import { mobileWebCheckArgs } from './run-mobile-web-app-checks.mjs'
import { MOBILE_WEB_APP_DEPENDENCIES_REQUIRED_ENV } from './mobile-web-app-bundle-dependencies.mjs'
@@ -105,15 +106,13 @@ describe('PR workflow parallelism', () => {
expect(nodeNextWorkflow.on.schedule).toHaveLength(1)
expect(nodeNextWorkflow.on.workflow_dispatch).toBeNull()
expect(sharedTest.strategy.matrix.node).toBe('${{ fromJSON(inputs.node_versions) }}')
expect(sharedTest.strategy.matrix.shard).toEqual(
Array.from({ length: 8 }, (_, index) => index + 1)
)
expect(sharedTest.strategy.matrix.shard_total).toEqual([8])
expect(sharedTest.strategy.matrix.shard).toBe('${{ fromJSON(needs.plan.outputs.shards) }}')
expect(sharedTest.needs).toBe('plan')
expect(installStep.with['node-version']).toBe('${{ matrix.node }}')
expect(installStep.with['cache-electron-package']).toBe('true')
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ matrix.shard_total }}')
expect(testStep.run).toContain('--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }}')
for (const testFile of nativeShellContractFiles) {
expect(testStep.run).toContain(`--exclude=${testFile}`)
expect(UNIT_EXCLUDE).toContain(testFile)
}
expect(primerInstall.with['native-runtime']).toBe('node')
expect(primerInstall.with['node-version']).toBe('24')
@@ -339,9 +338,10 @@ describe('PR workflow parallelism', () => {
(step) => step.uses === './.github/actions/install-node-dependencies'
)
for (const jobName of ['typecheck', 'git_compatibility', 'xterm_patch_sync']) {
for (const jobName of ['typecheck', 'git_compatibility']) {
expect(installFor(jobName).with, jobName).toBeUndefined()
}
expect(installFor('xterm_patch_sync')).toBeUndefined()
expect(installFor('static_analysis').with['native-runtime']).toBe('node')
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
expect(sharedTestInstall.with['native-runtime']).toBe('node')
@@ -456,7 +456,9 @@ describe('PR workflow parallelism', () => {
expect(electronCacheResolution.if).toBe(
"inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'"
)
expect(electronCache.if).toBe(electronCacheResolution.if)
expect(electronCache.if).toBe(
"(github.event_name != 'pull_request' || runner.os != 'Linux') && steps.electron-package-cache.outputs.version != ''"
)
expect(electronCache.uses).toBe('actions/cache@v5')
expect(electronCache.with.key).toContain('steps.electron-package-cache.outputs.version')
expect(dependencyAction.inputs['cache-electron-package'].default).toBe('false')
+64
View File
@@ -0,0 +1,64 @@
function reviewNumber(inputs) {
if (/^[1-9]\d*$/.test(inputs.pull_request_number ?? '')) {
return Number(inputs.pull_request_number)
}
const match =
/ \| PR ([1-9]\d*)$/.exec(inputs.name ?? '') ??
/^Review (?:new commits on )?#([1-9]\d*) \[[\w-]+\]$/.exec(inputs.name ?? '')
return match ? Number(match[1]) : null
}
function reviewRunOrder(runs, number, runId) {
const related = runs.filter((run) => reviewNumber({ name: run.display_title }) === number)
return {
superseded: related.some((run) => run.id > runId),
older: related
.filter(
(run) =>
run.id < runId &&
['queued', 'in_progress', 'waiting', 'pending', 'requested'].includes(run.status)
)
.map((run) => run.id)
}
}
async function reviewScope({ github, context, core }) {
const inputs = context.payload.inputs ?? {}
const number = reviewNumber(inputs)
core.setOutput('current', 'true')
if (!number || !Number.isSafeInteger(number)) {
return
}
try {
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number })
if (pr.state !== 'open' || (inputs.head_sha && inputs.head_sha !== pr.head.sha)) {
core.setOutput('current', 'false')
return
}
core.setOutput('head', pr.head.sha)
core.setOutput('number', String(number))
const { data } = await github.rest.actions.listWorkflowRuns({
...context.repo,
workflow_id: 'pullfrog.yml',
event: 'workflow_dispatch',
per_page: 100
})
const order = reviewRunOrder(data.workflow_runs, number, context.runId)
if (order.superseded) {
core.setOutput('current', 'false')
return
}
// Run IDs, unlike scope-job completion order, cannot let an older review cancel a newer one.
for (const runId of order.older) {
try {
await github.rest.actions.cancelWorkflowRun({ ...context.repo, run_id: runId })
} catch (error) {
core.warning(`Could not cancel older review ${runId}: ${error.message}`)
}
}
} catch (error) {
core.warning(`Review identity unavailable; leaving this task independent: ${error.message}`)
}
}
module.exports = { reviewNumber, reviewRunOrder, reviewScope }
@@ -0,0 +1,109 @@
import { expect, it, vi } from 'vitest'
import scope from './pullfrog-review-scope.cjs'
it('groups a current review by PR while leaving other agent tasks independent', async () => {
const get = vi.fn(async () => ({ data: { state: 'open', head: { sha: 'current' } } }))
const core = { setOutput: vi.fn(), warning: vi.fn() }
const context = {
repo: { owner: 'stablyai', repo: 'orca' },
runId: 7,
payload: { inputs: { name: 'Review #42 [abc]' } }
}
const github = {
rest: {
pulls: { get },
actions: { listWorkflowRuns: async () => ({ data: { workflow_runs: [] } }) }
}
}
await scope.reviewScope({ core, context, github })
expect(get).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', pull_number: 42 })
expect(core.setOutput).toHaveBeenCalledWith('head', 'current')
get.mockClear()
await scope.reviewScope({
core,
github,
context: { ...context, payload: { inputs: { name: 'Investigate #42' } } }
})
expect(get).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenLastCalledWith('current', 'true')
})
it('coalesces only explicit or recognized PR review identities', () => {
expect(scope.reviewNumber({ name: 'Review #23532 [85jpk]' })).toBe(23532)
expect(scope.reviewNumber({ name: 'Review new commits on #22727 [24lpq]' })).toBe(22727)
expect(scope.reviewNumber({ pull_request_number: '123' })).toBe(123)
for (const name of ['Fix #123', 'Review #12; echo test', 'Review #123', '', 'Review #0 [abc]']) {
expect(scope.reviewNumber({ name })).toBeNull()
}
})
it('skips closed or explicitly stale reviews and tolerates lookup failure', async () => {
for (const data of [
{ state: 'closed', head: { sha: 'a' } },
{ state: 'open', head: { sha: 'b' } }
]) {
const core = { setOutput: vi.fn(), warning: vi.fn() }
await scope.reviewScope({
core,
context: {
repo: {},
runId: 1,
payload: { inputs: { pull_request_number: '1', head_sha: 'a' } }
},
github: { rest: { pulls: { get: async () => ({ data }) } } }
})
expect(core.setOutput).toHaveBeenCalledWith('current', 'false')
}
const core = { setOutput: vi.fn(), warning: vi.fn() }
await scope.reviewScope({
core,
context: { repo: {}, runId: 2, payload: { inputs: { pull_request_number: '1' } } },
github: {
rest: {
pulls: {
get: async () => {
throw new Error('offline')
}
}
}
}
})
expect(core.setOutput).toHaveBeenCalledWith('current', 'true')
expect(core.warning).toHaveBeenCalled()
})
it('a delayed older scope cannot cancel or replace a newer review', async () => {
const runs = [
{ id: 6, display_title: 'Review #42 [a]', status: 'in_progress' },
{ id: 8, display_title: 'Custom review | PR 42', status: 'queued' },
{ id: 5, display_title: 'Investigate #42', status: 'in_progress' },
{ id: 4, display_title: 'Review #43 [other]', status: 'in_progress' }
]
for (const runId of [7, 9]) {
const cancelWorkflowRun = vi.fn(async () => ({}))
const core = { setOutput: vi.fn(), warning: vi.fn() }
await scope.reviewScope({
context: {
repo: { owner: 'stablyai', repo: 'orca' },
runId,
payload: { inputs: { name: 'Review #42 [current]' } }
},
core,
github: {
rest: {
pulls: { get: async () => ({ data: { state: 'open', head: { sha: 'head' } } }) },
actions: {
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
cancelWorkflowRun
}
}
}
})
if (runId === 7) {
expect(cancelWorkflowRun).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenCalledWith('current', 'false')
} else {
expect(cancelWorkflowRun.mock.calls.map(([args]) => args.run_id)).toEqual([6, 8])
}
}
})
@@ -236,6 +236,7 @@ function ensureDependencies(upstreamRoot, manifest) {
const stamp = path.join(upstreamRoot, 'node_modules', '.orca-xterm-install-stamp')
const want = `${manifest.upstream.commit}\n${statSync(lockfile).size}\n`
if (existsSync(stamp) && readFileSync(stamp, 'utf8') === want) {
assertToolchain(upstreamRoot, manifest)
return
}
run('npm', ['ci'], {
+138
View File
@@ -0,0 +1,138 @@
// Enforced by .github/workflows/release-policy.yml on release events.
const BOT_LOGIN = 'github-actions[bot]'
const BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com'
const NUMBER = '(?:0|[1-9][0-9]*)'
const VERSION = `${NUMBER}\\.${NUMBER}\\.${NUMBER}`
const STABLE_TAG = new RegExp(`^v${VERSION}$`)
const RC_TAG = new RegExp(`^v${VERSION}-rc\\.${NUMBER}(?:\\.[0-9A-Za-z]+)?$`)
const MOBILE_TAG = new RegExp(`^mobile(?:-android)?-v${VERSION}$`)
export function isPrereleaseTag(tag) {
return RC_TAG.test(tag) || MOBILE_TAG.test(tag)
}
export function compareStableTags(left, right) {
const a = left.slice(1).split('.').map(Number)
const b = right.slice(1).split('.').map(Number)
return a.reduce((result, part, index) => result || part - b[index], 0)
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
// Why: Cut Release creates the annotated tag and its `release: vX` commit, but the
// release object keeps whoever created the draft first, which can be a person.
export async function isCutByReleasePipeline(github, { owner, repo }, tag) {
if (!STABLE_TAG.test(tag) && !RC_TAG.test(tag)) {
return false
}
let ref
try {
;({ data: ref } = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }))
} catch (error) {
if (error.status === 404) {
return false
}
throw error
}
if (ref.object.type !== 'tag') {
return false
}
const { data: tagObject } = await github.rest.git.getTag({
owner,
repo,
tag_sha: ref.object.sha
})
if (tagObject.tagger?.email !== BOT_EMAIL || tagObject.object.type !== 'commit') {
return false
}
const { data: commit } = await github.rest.git.getCommit({
owner,
repo,
commit_sha: tagObject.object.sha
})
const subject = commit.message.split('\n')[0]
return new RegExp(`^release: ${escapeRegExp(tag)}(?: \\[rc-slot:[^\\]]+\\])?$`).test(subject)
}
async function isAuthorizedRelease(github, repoRef, release) {
const tag = release.tag_name
if (!STABLE_TAG.test(tag) && !isPrereleaseTag(tag)) {
return false
}
return release.author?.login === BOT_LOGIN || isCutByReleasePipeline(github, repoRef, tag)
}
// Why: the highest authorized stable release must be Latest; GitHub's own pick
// (and the old bot-author-only filter) can move it to an older version.
export async function restoreLatestStable(github, repoRef) {
const releases = await github.paginate(github.rest.repos.listReleases, {
...repoRef,
per_page: 100
})
const candidates = releases
.filter((release) => !release.draft && !release.prerelease && STABLE_TAG.test(release.tag_name))
.sort((left, right) => compareStableTags(right.tag_name, left.tag_name))
for (const release of candidates) {
if (!(await isAuthorizedRelease(github, repoRef, release))) {
continue
}
await github.rest.repos.updateRelease({
...repoRef,
release_id: release.id,
make_latest: 'true'
})
return release.tag_name
}
return null
}
export async function enforceReleasePolicy({ github, context, core }) {
const { release, action } = context.payload
const tag = release.tag_name
const author = release.author?.login
const repoRef = context.repo
const expectedPrerelease = isPrereleaseTag(tag)
if (await isAuthorizedRelease(github, repoRef, release)) {
if (release.prerelease !== expectedPrerelease) {
await github.rest.repos.updateRelease({
...repoRef,
release_id: release.id,
prerelease: expectedPrerelease,
make_latest: expectedPrerelease ? 'false' : 'legacy'
})
}
await restoreLatestStable(github, repoRef)
return
}
// Why: editing notes on a live release is not a publication; deleting on edit
// took down the v1.4.216 release and its tag.
if (action !== 'published') {
core.warning(
`Release ${tag} by ${author || 'unknown'} is outside release policy; left as-is on "${action}".`
)
return
}
await github.rest.repos.updateRelease({
...repoRef,
release_id: release.id,
draft: true,
prerelease: true,
make_latest: 'false'
})
await restoreLatestStable(github, repoRef)
await github.rest.repos.deleteRelease({ ...repoRef, release_id: release.id })
try {
await github.rest.git.deleteRef({ ...repoRef, ref: `tags/${tag}` })
} catch (error) {
if (error.status !== 404) {
throw error
}
}
core.warning(`Deleted unauthorized release ${tag} created by ${author || 'unknown'}.`)
}
+150
View File
@@ -0,0 +1,150 @@
import { describe, expect, it, vi } from 'vitest'
import { enforceReleasePolicy, restoreLatestStable } from './release-policy.mjs'
const BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com'
const repoRef = { owner: 'stablyai', repo: 'orca' }
function notFound() {
return Object.assign(new Error('Not Found'), { status: 404 })
}
// tags: { [tag]: { tagger, subject } } for annotated tags; missing tags 404.
function createGithub({ releases = [], tags = {} } = {}) {
const rest = {
repos: {
listReleases: vi.fn(),
updateRelease: vi.fn(async () => ({ data: {} })),
deleteRelease: vi.fn(async () => ({ data: {} }))
},
git: {
getRef: vi.fn(async ({ ref }) => {
const tag = ref.replace(/^tags\//, '')
if (!tags[tag]) {
throw notFound()
}
return { data: { object: { type: 'tag', sha: `tag-${tag}` } } }
}),
getTag: vi.fn(async ({ tag_sha }) => {
const tag = tag_sha.replace(/^tag-/, '')
return {
data: {
tagger: { email: tags[tag].tagger ?? BOT_EMAIL },
object: { type: 'commit', sha: `commit-${tag}` }
}
}
}),
getCommit: vi.fn(async ({ commit_sha }) => {
const tag = commit_sha.replace(/^commit-/, '')
return { data: { message: `${tags[tag].subject ?? `release: ${tag}`}\n` } }
}),
deleteRef: vi.fn(async () => ({ data: {} }))
}
}
return { rest, paginate: vi.fn(async () => releases) }
}
function release(tag, author, overrides = {}) {
return {
id: Number(tag.replace(/\D/g, '')),
tag_name: tag,
author: { login: author },
draft: false,
prerelease: false,
...overrides
}
}
function run(github, payloadRelease, action) {
const core = { warning: vi.fn() }
const context = { payload: { release: payloadRelease, action }, repo: repoRef }
return enforceReleasePolicy({ github, context, core }).then(() => core)
}
describe('release policy', () => {
it('keeps a pipeline-cut release whose draft a person created', async () => {
const r216 = release('v1.4.216', 'Jinwoo-H')
const github = createGithub({
releases: [r216, release('v1.4.214', 'github-actions[bot]')],
tags: { 'v1.4.216': {}, 'v1.4.214': {} }
})
await run(github, r216, 'edited')
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
expect(github.rest.git.deleteRef).not.toHaveBeenCalled()
expect(github.rest.repos.updateRelease).toHaveBeenCalledWith({
...repoRef,
release_id: r216.id,
make_latest: 'true'
})
})
it('never deletes an unauthorized release on edit', async () => {
const qa = release('v1.4.300', 'someone')
const github = createGithub({ releases: [qa] })
const core = await run(github, qa, 'edited')
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
expect(github.rest.git.deleteRef).not.toHaveBeenCalled()
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('left as-is'))
})
it('deletes a published release whose tag was not cut by the pipeline', async () => {
const qa = release('v1.4.300', 'someone')
const github = createGithub({
releases: [qa, release('v1.4.215', 'nwparker')],
tags: { 'v1.4.300': { tagger: 'someone@example.com' }, 'v1.4.215': {} }
})
await run(github, qa, 'published')
expect(github.rest.repos.deleteRelease).toHaveBeenCalledWith({ ...repoRef, release_id: qa.id })
expect(github.rest.git.deleteRef).toHaveBeenCalledWith({ ...repoRef, ref: 'tags/v1.4.300' })
})
it('rejects a bot-tagged tag whose commit is not a release commit', async () => {
const qa = release('v1.4.300', 'someone')
const github = createGithub({ tags: { 'v1.4.300': { subject: 'fix: something' } } })
await run(github, qa, 'published')
expect(github.rest.repos.deleteRelease).toHaveBeenCalled()
})
it('accepts scheduled RC release commits', async () => {
const rc = release('v1.4.217-rc.1', 'someone', { prerelease: true })
const github = createGithub({
tags: { 'v1.4.217-rc.1': { subject: 'release: v1.4.217-rc.1 [rc-slot:2026-09-28-15]' } }
})
await run(github, rc, 'published')
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
})
it('restores Latest to the highest pipeline-cut stable, not only bot-authored ones', async () => {
const github = createGithub({
releases: [
release('v1.4.214', 'github-actions[bot]'),
release('v1.4.215', 'nwparker'),
release('v1.4.216-rc.0', 'github-actions[bot]', { prerelease: true })
],
tags: { 'v1.4.214': {}, 'v1.4.215': {} }
})
await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.215')
expect(github.rest.repos.updateRelease).toHaveBeenCalledTimes(1)
})
it('compares versions numerically when picking Latest', async () => {
const github = createGithub({
releases: [
release('v1.4.99', 'github-actions[bot]'),
release('v1.4.100', 'github-actions[bot]')
]
})
await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.100')
})
})
@@ -388,7 +388,9 @@ async function main() {
child.kill('SIGTERM')
const exited = await Promise.race([
new Promise((r) => child.on('exit', () => r(true))),
new Promise((r) => setTimeout(() => r(false), SHUTDOWN_TIMEOUT_MS))
// unref'd: the loser of this race must not hold the event loop open after the
// winner already decided. The timer still bounds the wait.
new Promise((r) => setTimeout(() => r(false), SHUTDOWN_TIMEOUT_MS).unref())
])
if (!exited) {
child.kill('SIGKILL')
@@ -0,0 +1,72 @@
import { readFileSync } from 'node:fs'
import { expect, it } from 'vitest'
import { parse } from 'yaml'
const readYaml = (path) => parse(readFileSync(new URL(path, import.meta.url), 'utf8'))
const action = readYaml('../../.github/actions/prepare-xterm-dependencies/action.yml')
const workflow = readYaml('../../.github/workflows/ci-xterm-cache.yml')
const restore = action.runs.steps.find((step) => step.id === 'restore')
it('separates toolchains and restores dependencies without a stale fallback or build results', () => {
for (const input of [
'runner.os',
'runtime.outputs.image',
'runner.arch',
'runtime.outputs.node',
'xterm-upstream.json',
'regenerate-xterm-patches.mjs',
'xterm-patch-text.mjs',
'prepare-xterm-dependencies/action.yml'
]) {
expect(restore.with.key).toContain(input)
}
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore.with['restore-keys']).toBeUndefined()
expect(restore.with.path.trim().split('\n')).toEqual([
'${{ runner.temp }}/xterm-patch-build/upstream/.git',
'${{ runner.temp }}/xterm-patch-build/upstream/node_modules'
])
})
it('publishes only from main after a successful fresh verification, and skips work on hits', () => {
expect(workflow.on.pull_request).toBeUndefined()
expect(workflow.on.push.branches).toEqual(['main'])
expect(workflow.jobs.seed.if).toBe("github.ref == 'refs/heads/main'")
expect(workflow.permissions).toEqual({ contents: 'read' })
const steps = workflow.jobs.seed.steps
const verify = steps.find((step) => step.name === 'Verify and populate dependencies')
const save = steps.find((step) => step.uses === 'actions/cache/save@v5')
expect(verify.if).toBe("steps.cache.outputs.cache-hit != 'true'")
expect(save.if).toBe(verify.if)
expect(save.with.path).toBe(restore.with.path)
expect(save.with.key).toBe('${{ steps.cache.outputs.cache-key }}')
expect(steps.indexOf(save)).toBeGreaterThan(steps.indexOf(verify))
expect(verify.run).toContain('regenerate-xterm-patches.mjs --check')
const pr = readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync.steps
expect(workflow.jobs.seed['runs-on']).toBe(
readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync['runs-on']
)
expect(pr.some((step) => step.uses === './.github/actions/prepare-xterm-dependencies')).toBe(true)
expect(pr.some((step) => /cache(?:\/save)?@/.test(step.uses ?? ''))).toBe(false)
expect(pr.at(-1).run).toContain('regenerate-xterm-patches.mjs --check')
})
it('restores download fallback only on a miss and preserves its established key and paths', () => {
const fallback = action.runs.steps.at(-1)
const save = workflow.jobs.seed.steps.at(-1)
expect(fallback.if).toBe("steps.restore.outputs.cache-hit != 'true'")
expect(fallback.uses).toBe('actions/cache/restore@v5')
expect(fallback.with).toEqual(save.with)
expect(save.if).toBe("steps.cache.outputs.cache-hit != 'true'")
})
it('runs the standalone generator without installing unrelated Orca dependencies', () => {
const steps = readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync.steps
expect(steps.some((step) => step.uses === './.github/actions/install-node-dependencies')).toBe(
false
)
expect(steps.find((step) => step.uses === 'actions/setup-node@v6').with).toEqual({
'node-version-file': 'package.json',
'package-manager-cache': false
})
})
+7 -1
View File
@@ -6,7 +6,9 @@
"../src/main/agent-state-file-reader.ts",
"../src/main/agent-hooks/grok-replay-guard.ts",
"../src/main/claude/hook-script.ts",
"../src/main/claude/claude-session-end-hook-capability.ts",
"../src/main/claude/claude-hook-event-versions.ts",
"../src/main/claude/claude-managed-hook-events.ts",
"../src/main/qoder/hook-service.ts",
"../src/main/agent-hooks/hook-stdin-contract.ts",
"../src/main/agent-hooks/hook-post-command.ts",
"../src/main/agent-hooks/hook-config-write-path.ts",
@@ -18,6 +20,7 @@
"../src/main/agent-hooks/managed-agent-hook-controls.ts",
"../src/main/agent-hooks/managed-agent-hook-registry.ts",
"../src/main/agent-hooks/managed-hook-script-refresh.ts",
"../src/main/agent-hooks/managed-hooks-json-events.ts",
"../src/main/agent-hooks/posix-hook-command.ts",
"../src/main/agent-hooks/runtime-home-hook-command.ts",
"../src/main/orca-profiles/profile-storage-paths.ts",
@@ -207,6 +210,9 @@
"../src/main/in-flight-run-dedupe.ts",
"../src/main/kimi/hook-service.ts",
"../src/main/kimi/kimi-hook-config-toml.ts",
"../src/main/dsh/dsh-home-patch.ts",
"../src/main/dsh/hook-service.ts",
"../src/main/dsh/hook-settings.ts",
"../src/main/muse/hook-config-json.ts",
"../src/main/muse/hook-service.ts",
"../src/main/muse/hook-settings.ts",
+2
View File
@@ -6,6 +6,8 @@
"./scripts/vitest-host-ports-setup.ts",
"../src/main/**/*",
"../src/renderer/src/lib/skill-freshness-display-status.ts",
"../src/renderer/src/components/native-chat/native-chat-resolution-receipt.ts",
"../src/renderer/src/components/native-chat/structured-agent-question-projection.ts",
"../src/preload/**/*",
"../src/shared/**/*",
"../src/relay/**/*",
+3 -8
View File
@@ -1,5 +1,6 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vitest/config'
import { UNIT_INCLUDE, UNIT_EXCLUDE } from './scripts/ci-unit-files.mjs'
import TimingSequencer from './scripts/ci-unit-sequencer.mjs'
const windowsTestWorkerOptions = process.platform === 'win32' ? { maxWorkers: 4 } : {}
@@ -33,14 +34,8 @@ export default defineConfig({
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
resolve('config/scripts/vitest-host-ports-setup.ts')
],
include: [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'config/scripts/**/*.test.ts',
'config/scripts/**/*.test.mjs',
'tests/tools/**/*.test.mjs',
'tests/e2e/**/*.unit.test.ts'
],
include: UNIT_INCLUDE,
...(process.env.ORCA_BALANCE_UNIT_SHARDS === '1' ? { exclude: UNIT_EXCLUDE } : {}),
// Why: the full suite runs heavy TS transforms plus real git/http fixtures;
// the Vitest 5s defaults are too tight for the slowest integration cases.
hookTimeout: 60_000,
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 81m">
<title>downloads: 81m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 82m">
<title>downloads: 82m</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
<text x="37" y="14">downloads</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">81m</text>
<text x="90" y="14">81m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">82m</text>
<text x="90" y="14">82m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 384 KiB

+2
View File
@@ -179,6 +179,7 @@ Funciona con **cualquier agente CLI** — si corre en una terminal, corre en Orc
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a> &nbsp;
@@ -193,6 +194,7 @@ Funciona con **cualquier agente CLI** — si corre en una terminal, corre en Orc
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
+4 -2
View File
@@ -40,7 +40,7 @@
Surveillez et pilotez vos agents depuis votre téléphone — soyez notifié quand un agent termine, et envoyez des instructions de suivi où que vous soyez.
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -183,6 +183,7 @@ Fonctionne avec **n'importe quel agent CLI** — s'il tourne dans un terminal, i
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Logo Cursor" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="Logo GitHub Copilot" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Logo Muse" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="Logo ZCode" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="Logo OpenCode" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="Logo MiMo Code" width="16" valign="middle" /> MiMo Code</kbd></a> &nbsp;
@@ -199,6 +200,7 @@ Fonctionne avec **n'importe quel agent CLI** — s'il tourne dans un terminal, i
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Logo Charm" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Logo Cline" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Logo Codebuff" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Logo Freebuff" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Logo Command Code" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Logo Continue" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Logo Droid" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
@@ -236,7 +238,7 @@ yay -S stably-orca-bin
Associez-la à l'app de bureau pour surveiller et piloter vos agents depuis votre téléphone.
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [rejoindre TestFlight](https://testflight.apple.com/join/YjeGMQBA)
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android :** [Télécharger l'APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
---
+2
View File
@@ -179,6 +179,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a> &nbsp;
@@ -193,6 +194,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
+4 -2
View File
@@ -36,7 +36,7 @@
휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다.
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -179,6 +179,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="MiMo Code logo" width="16" valign="middle" /> MiMo Code</kbd></a> &nbsp;
@@ -195,6 +196,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요.
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217) 또는 [TestFlight 참여](https://testflight.apple.com/join/YjeGMQBA)
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [APK 0.0.50 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
---
+4 -2
View File
@@ -36,7 +36,7 @@
Monitore e conduza seus agentes pelo celular — receba uma notificação quando um agente terminar e envie instruções de acompanhamento de qualquer lugar.
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -179,6 +179,7 @@ Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca.
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Logotipo do Cursor" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="Logotipo do GitHub Copilot" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Logotipo do Muse" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="Logotipo do ZCode" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="Logotipo do OpenCode" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="Logotipo do MiMo Code" width="16" valign="middle" /> MiMo Code</kbd></a> &nbsp;
@@ -195,6 +196,7 @@ Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca.
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Logotipo do Charm" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Logotipo do Cline" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Logotipo do Codebuff" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Logotipo do Freebuff" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Logotipo do Command Code" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Logotipo do Continue" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Logotipo do Droid" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
Conecte ao app desktop para monitorar e conduzir seus agentes pelo celular.
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [entrar no TestFlight](https://testflight.apple.com/join/YjeGMQBA)
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [Baixar APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
---
+2
View File
@@ -179,6 +179,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a> &nbsp;
@@ -193,6 +194,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a> &nbsp;
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a> &nbsp;
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a> &nbsp;
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a> &nbsp;
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a> &nbsp;
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a> &nbsp;
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a> &nbsp;
+145
View File
@@ -0,0 +1,145 @@
# CI demand rollout
This implements the September 28 runner-demand analysis. The baseline inventory
covered September 27 04:00–September 28 04:00 UTC: 4,028 workflow runs, with
463 stratified job samples. Estimated occupancy was 1,081 runner-hours, dominated
by PR unit shards (414 hours) and Bun qualification (262 hours). These are
sampled sums of job durations across different runner pools, not billing totals
or a guaranteed forecast of savings.
## What runs now
| Work | Ordinary draft update | Ready PR / final checks | Main reference |
| ---------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------- | --------------------------------------- |
| Static analysis and types | Immediately | Immediately | Existing workflows |
| Unit suite | Full, with shadow selection evidence | Full | Existing daily Node 24/26 x86 suite |
| Packages | After successful static analysis and types | Same | Existing release workflows |
| Bun persistence | Linux x64 for ordinary runtime changes; all six platforms for sensitive inputs | All six platforms when Bun inputs changed | Full nightly qualification at 11:30 UTC |
| Bun glibc/musl qualification | Sensitive inputs only, after persistence succeeds | Both architectures after persistence succeeds | Both architectures |
| E2E | Existing targeted routing | Existing targeted routing | One complete run at 17:00 UTC |
Bun-sensitive inputs include root/toolchain files, configuration, native code,
resources, platform-specific paths, persistence, SQLite, orcad, providers,
daemon, SSH, relay, and child-process code. Dependency discovery failure, a
missing event or an incomplete diff retains full qualification. An unrelated
change still skips Bun through the existing dependency classifier. No native
artifact is shared across platforms or ABIs. The routine draft reduction is an
explicit coverage-placement change; it does not assert identical per-update
coverage. Every non-draft synchronize event and the ready-for-review event
restores full qualification.
Expensive PR jobs wait for static/type success. This reduces fan-out for failed
or rapidly superseded commits without sleeping on a runner. Successful isolated
PRs pay the extra stage latency. Existing per-PR cancellation remains in place.
Package assertions, native boundaries, SSH/folder coverage, cache warming and
slow-test assertions are retained.
## Unit selection rollout
`ci-unit-plan.mjs` discovers the same include/exclude set as Vitest and follows
static imports, re-exports, literal dynamic imports, CommonJS requires and the
renderer aliases. Consumers of indirect filesystem/process inputs remain in the
candidate set, as do script/tool tests. Global configuration changes, deletions,
renames involving removed paths, unknown inputs and graph failures run the full
suite. A shard verifies the plan's source SHA and complete discovery list before
using it. Missing/stale artifacts fall back to full coverage, even if that means
running the suite on fewer shards.
The initial policy is **shadow**, with all eight shards retained. The first
local inventory matched Vitest exactly (9,950 files at validation); representative
source changes retained roughly 88% of files because of indirect input readers.
That is evidence for conservative coverage, not evidence of the analysis's
hypothetical 50% unit-work reduction. Improvements to indirect dependency
modeling should be demonstrated against full results before expanding selection.
Every shard uploads `unit-selection.json`, `unit-timings.json` (including module
outcomes), and its assignment. The evidence job combines these into
`unit-selection-review-attempt-N/selection-review.json`, reporting:
- Whether every discovered file appeared once across a complete reference run.
- Failures outside the candidate set, including failures in otherwise red runs.
- Measured worker time that selection would omit; worker times overlap and are
not runner occupancy or a prediction of wall-clock savings.
Missing/duplicate shards, stale plans, interrupted runs and unhandled errors do
not count as complete references. Diagnostic upload/report failures do not make
tests pass and do not independently fail successful tests.
After representative complete shadow runs show no missed failures, set repository
variable `ORCA_UNIT_SELECTION_MODE=selected` to enable selection **only for draft
PRs**. Keep full ready-PR checks and the daily compatibility suite. Inspect at
least a week's evidence across renderer, main, shared, SSH and fixture changes
before promotion, including red runs rather than only successful examples.
Unknown variable values retain shadow mode. Unset the variable or set it to
`shadow` to roll back immediately. Selected runs use one to eight timing-balanced
shards based on retained work.
Ready-for-review result reuse includes `unit full` in its source/workflow
identity. A green selected draft cannot satisfy the final full check, even if
the repository variable changes between runs. An already successful _full_
identical-source check can still be reused.
To inspect downloaded shard artifacts locally:
```sh
node config/scripts/ci-unit-selection-review.mjs ARTIFACT_DIRECTORY
```
## Review automation
Pullfrog recognizes the existing `Review #N [id]` and
`Review new commits on #N [id]` dispatch names. Explicit dispatchers may provide
`pull_request_number` and `head_sha`. Explicit PR identities share concurrency at the workflow boundary. Legacy review
names use a bounded lookup of the latest 100 dispatches and cancel only lower
run IDs for the same PR; a delayed older scope cannot cancel a newer review.
Unrecognized tasks are never grouped. The scope job alone has Actions write
permission for ordered cancellation. Closed PRs and explicitly stale heads
are skipped. A second head check prevents starting an agent after its queued
head has changed. Unrecognized agent tasks remain independent; lookup failures
also retain an independent task rather than cancelling unrelated work.
This does not introduce a fixed debounce interval or remove final reviews.
Dispatchers should supply `head_sha` for reliable stale-at-dispatch detection;
legacy names identify a PR but do not prove which head the prompt describes.
## E2E signal
The daily reference still executes all shards and keeps original verdicts. Each
shard uploads Playwright JSON and publishes expected, skipped, unexpected, flaky
and startup-error counts with the failing test names/messages. Targeted PR and
manual coverage remain available. This change does not fix the historically
red tests or pretend they pass.
`config/e2e-failure-tracking.json` can separate an evidenced repeated failure
from new failures in the summary. Each entry must have exact `file`, full
`title`, `project`, a nonempty stable `message` substring, an `@owner`, a linked
repository `issue`, and an ISO `expires` review date. Expired/malformed entries
are ignored and reported; changed error signatures appear as untracked. Entries
never skip a test or change its exit status. The initial list is empty because
the analysis established red workflows but did not establish owners and
reproductions for individual failures. Do not blanket-baseline an entire red run.
## Capacity measurements and acceptance
`CI runner demand` runs daily at 04:23 UTC and can be dispatched manually. It
reads the previous 24 complete hours in hourly pages, samples up to six runs per
workflow/outcome stratum, and fetches job pages with bounded concurrency. An
hour exceeding the API's 1,000-result search cap fails visibly. The report and
raw evidence are retained for 30 days. No extra runner pool is provisioned.
The report measures the full job durations of runs **created** in the window,
not occupancy clipped to the window: earlier runs that overlap it are excluded,
and completed sampled jobs may finish after it. This matches the baseline
cohort method. Workflow IDs keep ref-qualified paths in one sampling stratum.
The report shows weighted runner-hours and cancelled-run hours per workflow,
runner-minutes per completed PR _run_, and weighted queue/provisioning p95 per
runner label. It counts latest attempts only, excludes incomplete jobs, and
retains zero-job observations. It does not measure other repositories competing
for organization capacity. Compare equivalent traffic windows, not raw totals
alone. The collector needs only `contents: read` and `actions: read`.
After a week, compare runner-minutes per PR run, cancellation occupancy and
queue p95 in each affected pool. Count newly added planning/reference overhead.
A 25–35% overall reduction remains an experiment target, not an achieved result;
selection, coalescing and matrix reductions overlap and cannot simply be added.
+3
View File
@@ -1,5 +1,8 @@
# CI efficiency and runner capacity
The [September 28 demand rollout](ci-demand-rollout.md) documents staged checks,
unit-selection evidence, Bun qualification, review cancellation and daily occupancy reports.
## September 27 follow-up
### Shared E2E CLI output
+89
View File
@@ -0,0 +1,89 @@
# Qoder CLI integration
Orca registers `qodercli` as `qoder`: detection, picker/settings, desktop and mobile
identity, prompt launch, permission flags, managed hooks, workspace trust, and
session resume use the existing TUI-agent and hook-status paths.
## Verified contract
Verified on macOS with Qoder CLI 1.1.64, including its versioned executable
`qodercli-1.1.64`:
- `--prompt-interactive` starts an interactive prompt; `--resume <id>` resumes a
session; `--dangerously-skip-permissions` is the permission bypass flag.
- Hooks use the Claude-shaped nested configuration in `~/.qoder/settings.json`.
Orca registers its own `/hook/qoder` source and preserves user hooks.
- Real SessionStart, UserPromptSubmit, Notification and SessionEnd events are
captured in `src/shared/__fixtures__/qoder-no-account-hooks.jsonl`, including
`source: resume` with the original session ID.
- Trust is `permissions.trustDirectories` in that settings file, using the
canonical workspace path. A workspace `.trusted` marker did not bypass the
trust dialog in this version. Remote trust uses the execution host filesystem.
- Qoder emits its `◇ … | Ready` title while the trust menu owns input. Readiness
therefore requires the live composer text, not merely this title or silence.
Raw PTY fixtures under `src/main/runtime/__fixtures__/qoder-*` cover trust,
unauthenticated prompt handling and the ready composer.
- A hidden Orca dev instance launched Qoder from New Tab in a folder workspace.
Its icon, label, terminal rendering and failed-turn indicator were inspected.
A harmless prompt reached Qoder, which reported its credit usage limit; the
canonical hook store recorded Qoder identity, session metadata and failure.
## Compatibility and limits
Windows hooks explicitly select Qoder's documented PowerShell shell, avoiding
an assumption that Qoder uses Git Bash just because it is installed. They reuse
the existing managed `.cmd` payload without adding an encoded PowerShell hop.
The configuration shape and preservation are tested; Windows, Linux and SSH
execution have not been exercised live here.
Resume requests to remote hosts require `agent-session.qoder-resume.v1`, so an
older host is not sent an agent enum it cannot accept. Remote hook installation
and trust preservation have automated coverage.
Successful model output, tool execution and live permission dialogs remain
unverified because the available account has exhausted its credits. Hook event
mapping for these paths follows the official documentation. The China executable
`qoderclicn` is not registered; it was not available for verification.
New Tab follows the existing manual-launch trust behavior: an untrusted folder
can show Qoder's trust dialog. Automated workspace/draft launch paths run the
trust preflight before prompt delivery.
## Sources
- [Official hooks documentation](https://docs.qoder.com/cli/hooks)
- [#15291](https://github.com/stablyai/orca/pull/15291): registration and hook proposal
- [#13311](https://github.com/stablyai/orca/pull/13311): icon asset (commit
`b56197025530adb1b82d97a364a8c3d4a02c0d42`) and agent integration
- [#8611](https://github.com/stablyai/orca/pull/8611) and
[#12910](https://github.com/stablyai/orca/pull/12910): contributor implementations
- [#16540](https://github.com/stablyai/orca/issues/16540): Qoder misidentified as Gemini
Contributor patches informed the integration; they were not applied wholesale.
In particular, the trust marker proposal was replaced using the installed CLI's
observed settings write, and no renderer workaround was added because the live
WebGL terminal rendered correctly.
## Cross-review of open proposals
Reviewed the current patches for all six open Qoder PRs on 2026-09-28:
| PR | Incorporated or confirmed | Deliberate differences |
| --- | --- | --- |
| #7502 (vincent-lxc) | Agent registration, mobile parity, identity before Gemini | Use structural titles and the current public permission flag. |
| #8611 (Eridanus117, building on #7502) | Qoder hook source, notification/permission mapping, session resume | 1.1.64 supports `--prompt-interactive`; preserve startup session identity and ignore compact restarts. |
| #9655 (xingqingzzp-gif) | Cross-checked minimum registration coverage | Placeholder icon and detection-only scope are superseded by the fuller integration; no code copied. |
| #12910 (jyang2004) | Reuse the Claude-compatible installer with an event subset, plus remote installation | Use observed `.qoder` path and `/hook/qoder`; Qwen and the China build remain separate work. |
| #13311 (sorrycc) | Icon, structural title disambiguation, headless detection, display glyph cleanup | No DOM renderer override: 1.1.64 renders correctly in the live WebGL terminal. No unverified executable alias. |
| #15291 (adlternative) | Versioned binary recognition, trust preflight wiring, skills mapping and resume | Trust comes from settings, not `.trusted`; include notifications, permission and failure events with Qoder-specific normalization. |
The rendered left sidebar was checked in the hidden Electron app using a temporary
folder workspace and real Qoder hooks. The workspace fixture needed a parent path
on its project group to appear in the sidebar. No agent status was injected into
the renderer. A real prompt updated the row text and returned a credit-limit error;
the row and workspace card showed failure with the Qoder icon. A DOM observer
recorded the prompt-row transition before failure. Permission/waiting and successful
completion remain automated event-mapping tests, not live model/tool verification.
Commit co-authors credit all six proposal authors for the implementation and
registration groundwork, including xingqingzzp-gif’s minimal registration proposal.
@@ -46,8 +46,10 @@ To restore prompts for one agent only, edit that agent's default arguments or en
| Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) |
| Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) |
| Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) |
| Freebuff | Auto-setup | [Freebuff](https://freebuff.com/cli) |
| Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) |
| Muse | macOS/Linux; trusts the workspace at launch | [Meta](https://dev.meta.ai/docs/muse-code) |
| DeepSeek Harness | Launched through its `dsh-tui` profile; hooks, status, questions, resume | [DeepSeek](https://deepseek-harness.github.io/deepseek-harness/) |
| ZCode | Deep integration; needs a `zcode` CLI that ships the TUI (see note below) | [Z.ai](https://zcode.z.ai/en/docs) |
| Continue | Auto-setup | [Continue](https://docs.continue.dev/guides/cli) |
| Cursor CLI | Deep integration | [Cursor](https://cursor.com/cli) |
+2 -1
View File
@@ -19,6 +19,7 @@ import { displayHostEndpoint } from '../../../src/transport/host-endpoint'
import { resolveHostEndpointEdit } from '../../../src/transport/host-endpoint-edit'
import { usePrimeHosts, useRefreshHostClient } from '../../../src/transport/client-context'
import type { HostProfile } from '../../../src/transport/types'
import { hostOs } from '../../../src/platform/host-os'
export default function EditHostScreen() {
const router = useRouter()
@@ -179,7 +180,7 @@ export default function EditHostScreen() {
) : (
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
behavior={hostOs() === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={[styles.form, { paddingBottom: insets.bottom + spacing.xl }]}
@@ -0,0 +1,50 @@
import ObjectiveC
import UIKit
import WebKit
/// Removes WKWebView's form accessory bar (up, down, done). Native screens have none, and the
/// keyboard event the shell publishes counts it, so the page lifted by the bar as well. WebKit
/// exposes no switch, so the content view is moved onto a subclass whose `inputAccessoryView` is
/// nil, the same technique react-native-webview's `hideKeyboardAccessoryView` uses.
func hideKeyboardAccessoryBar(of webView: WKWebView) {
guard
let content = webView.scrollView.subviews.first(where: {
String(describing: type(of: $0)).hasPrefix("WKContent")
})
else {
return
}
let base: AnyClass = type(of: content)
let name = "\(NSStringFromClass(base))_OrcaNoAccessory"
let subclass: AnyClass
if let existing = NSClassFromString(name) {
subclass = existing
} else {
guard let created = objc_allocateClassPair(base, name, 0) else {
return
}
let noAccessory: @convention(block) (AnyObject) -> UIView? = { _ in nil }
let selector = #selector(getter: UIResponder.inputAccessoryView)
let typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIResponder.self, selector)!)
class_addMethod(created, selector, imp_implementationWithBlock(noAccessory), typeEncoding)
objc_registerClassPair(created)
subclass = created
}
object_setClass(content, subclass)
}
/// Stops WKWebView reacting to the keyboard itself. It listens for the keyboard's frame and scrolls
/// the document to reveal the focused field, on top of the lift the page already applies from the
/// height the shell sends, so the page moved twice. The shell's own listeners are unaffected.
func ignoreKeyboardNotifications(in webView: WKWebView) {
for name in [
UIResponder.keyboardWillShowNotification,
UIResponder.keyboardDidShowNotification,
UIResponder.keyboardWillHideNotification,
UIResponder.keyboardDidHideNotification,
UIResponder.keyboardWillChangeFrameNotification,
UIResponder.keyboardDidChangeFrameNotification
] {
NotificationCenter.default.removeObserver(webView, name: name, object: nil)
}
}
@@ -221,6 +221,8 @@ final class OrcaMobileWebShellView: ExpoView, WKNavigationDelegate, WKUIDelegate
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
webView.scrollView.contentInsetAdjustmentBehavior = .never
hideKeyboardAccessoryBar(of: webView)
ignoreKeyboardNotifications(in: webView)
webView.translatesAutoresizingMaskIntoConstraints = false
addSubview(webView)
NSLayoutConstraint.activate([
@@ -3,7 +3,7 @@
"family": "aiVault.history",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
@@ -3,7 +3,7 @@
"family": "aiVault.history",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
@@ -3,7 +3,7 @@
"family": "aiVault.history",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
@@ -3,7 +3,7 @@
"family": "aiVault.history-screen",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3",
@@ -3,7 +3,7 @@
"family": "aiVault.history-screen",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3",
@@ -3,7 +3,7 @@
"family": "aiVault.resume-launch",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",
@@ -3,7 +3,7 @@
"family": "aiVault.resume-launch",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",
@@ -3,7 +3,7 @@
"family": "aiVault.resume-launch",
"namedDeltas": [],
"runnerVersion": 1,
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",

Some files were not shown because too many files have changed in this diff Show More