Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,10 @@ jobs:
run: |
# Bare shells install Pi extensions only for an existing agent home.
mkdir -p "$HOME/.pi/agent"
# sshd login shells do not inherit setup-node's toolcache PATH.
mkdir -p "$HOME/.local/bin"
ln -sf "$(command -v node)" "$HOME/.local/bin/node"
ln -sf "$(command -v npm)" "$HOME/.local/bin/npm"
fixture="$RUNNER_TEMP/orca-localhost-sshd"
mkdir -p "$fixture"
ssh-keygen -q -t ed25519 -N '' -f "$fixture/host_key"
Expand Down
24 changes: 12 additions & 12 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
test: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.test }}
orcad_browser: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.orcad_browser }}
cross-version-wire: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.cross-version-wire }}
managed_hook_node18: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node18 }}
managed_hook_node24: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node24 }}
package: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package }}
package_windows: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package_windows }}
e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }}
Expand Down Expand Up @@ -979,15 +979,15 @@ jobs:
- name: Old/new client and server compatibility journeys
run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/

managed_hook_node18:
name: managed hooks on Node 18
managed_hook_node24:
name: managed hooks on Node 24
needs: [code_paths, preflight]
if: >-
!cancelled() &&
needs.code_paths.result == 'success' &&
needs.code_paths.outputs.managed_hook_node18 == 'true' &&
needs.code_paths.outputs.managed_hook_node24 == 'true' &&
needs.preflight.result == 'success'
# Why ARM: Node 18 publishes linux-arm64; the per-platform runtime files are read as data.
# Why ARM: Node 24 publishes linux-arm64; the per-platform runtime files are read as data.
runs-on: ubuntu-24.04-arm

steps:
Expand All @@ -1001,13 +1001,13 @@ jobs:
- name: Build relay companions
run: pnpm run build:relay

- name: Setup Node 18 runtime
- name: Setup Node 24 runtime
uses: actions/setup-node@v6
with:
node-version: '18'
node-version: '24'

- name: Smoke managed-hook companions
run: node config/scripts/smoke-managed-hook-runtime-node18.mjs
run: node config/scripts/smoke-managed-hook-runtime-node24.mjs

package:
name: package
Expand Down Expand Up @@ -1410,7 +1410,7 @@ jobs:
- orcad_browser
- mobile_web_app
- cross-version-wire
- managed_hook_node18
- managed_hook_node24
- package
- package_windows
# Evaluating job results only needs the lightweight container runner.
Expand Down Expand Up @@ -1448,8 +1448,8 @@ jobs:
MOBILE_WEB_APP_SHOULD_RUN: ${{ needs.code_paths.outputs.mobile_web_app }}
CROSS_VERSION_WIRE: ${{ needs.cross-version-wire.result }}
CROSS_VERSION_WIRE_SHOULD_RUN: ${{ needs.code_paths.outputs.cross-version-wire }}
MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }}
MANAGED_HOOK_NODE18_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node18 }}
MANAGED_HOOK_NODE24: ${{ needs.managed_hook_node24.result }}
MANAGED_HOOK_NODE24_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node24 }}
PACKAGE: ${{ needs.package.result }}
PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }}
PACKAGE_WINDOWS: ${{ needs.package_windows.result }}
Expand Down Expand Up @@ -1486,7 +1486,7 @@ jobs:
check_job orcad_browser "$ORCAD_BROWSER" "$ORCAD_BROWSER_SHOULD_RUN"
check_job mobile_web_app "$MOBILE_WEB_APP" "$MOBILE_WEB_APP_SHOULD_RUN"
check_job cross-version-wire "$CROSS_VERSION_WIRE" "$CROSS_VERSION_WIRE_SHOULD_RUN"
check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN"
check_job managed_hook_node24 "$MANAGED_HOOK_NODE24" "$MANAGED_HOOK_NODE24_SHOULD_RUN"
check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN"
check_job package_windows "$PACKAGE_WINDOWS" "$PACKAGE_WINDOWS_SHOULD_RUN"
exit "$failed"
36 changes: 18 additions & 18 deletions config/reliability-gates.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20285,8 +20285,8 @@
"demotionRule": "Keep experimental or demote if any case terminates on a signal, the preconditions stop holding on the CI runner, or an assertion can be satisfied by anything other than the command's own output."
},
{
"id": "ssh-managed-hooks.node18-runtime-compatibility",
"title": "SSH managed-hook companions load and install hooks on Node 18",
"id": "ssh-managed-hooks.node24-runtime-compatibility",
"title": "SSH managed-hook companions load and install hooks on Node 24",
"maturity": "experimental",
"protection": "partial",
"owner": "agent-hooks",
Expand All @@ -20300,43 +20300,43 @@
"providers": ["ssh"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["ssh"],
"coverageNotes": "A real Node 18 process loads every platform's built companion and runs the bundled Codex and Claude installers against an isolated temporary home. The six artifacts share one platform-neutral source bundle; live SSH transport and physical remote hosts remain uncollected.",
"coverageNotes": "A real Node 24 process loads every platform's built companion and runs the bundled Codex and Claude installers against an isolated temporary home. The six artifacts share one platform-neutral source bundle; live SSH transport and physical remote hosts remain uncollected.",
"motivatingLinks": ["https://github.com/stablyai/orca/pull/12617"],
"invariant": "Every managed-hook companion shipped for Node 18 must load before agent installers execute, and importing desktop-only state inspection must not make unrelated remote installers depend on node:sqlite.",
"oracle": "Build all relay artifacts, require each managed-hook-runtime.js in a real Node 18 process, assert its installer export, then install only Codex and Claude into a fresh temporary home. Require two successful installer results, executable hook scripts, and both agent configs to reference their managed scripts without touching the user's home.",
"invariant": "Every managed-hook companion shipped for Node 24 must load before agent installers execute, and importing desktop-only state inspection must not make unrelated remote installers depend on node:sqlite.",
"oracle": "Build all relay artifacts, require each managed-hook-runtime.js in a real Node 24 process, assert its installer export, then install only Codex and Claude into a fresh temporary home. Require two successful installer results, executable hook scripts, and both agent configs to reference their managed scripts without touching the user's home.",
"commands": [
"pnpm run build:relay && npx -y node@18 config/scripts/smoke-managed-hook-runtime-node18.mjs",
"pnpm run build:relay && node config/scripts/smoke-managed-hook-runtime-node24.mjs",
"pnpm exec vitest run --config config/vitest.config.ts config/scripts/pr-workflow-parallelism.test.mjs"
],
"testFiles": [
"config/scripts/smoke-managed-hook-runtime-node18.mjs",
"config/scripts/smoke-managed-hook-runtime-node24.mjs",
"config/scripts/pr-workflow-parallelism.test.mjs"
],
"assertionRefs": [
{
"file": "config/scripts/smoke-managed-hook-runtime-node18.mjs",
"file": "config/scripts/smoke-managed-hook-runtime-node24.mjs",
"assertions": [
"all six built companions load and export installManagedHooks under Node 18",
"all six built companions load and export installManagedHooks under Node 24",
"Codex and Claude installations both succeed in an isolated home",
"both generated configs reference executable managed hook scripts"
]
},
{
"file": "config/scripts/pr-workflow-parallelism.test.mjs",
"assertions": [
"PR CI builds the companions before switching to Node 18 and running the smoke oracle"
"PR CI builds the companions before switching to Node 24 and running the smoke oracle"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-08",
"date": "2026-10-07",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm run build:relay && npx -y node@18 config/scripts/smoke-managed-hook-runtime-node18.mjs",
"durationSeconds": 4,
"summary": "All six companions loaded under Node 18.20.8; Codex and Claude produced two successful results, executable hook scripts, and managed configs in a temporary home."
"command": "pnpm run build:relay && node config/scripts/smoke-managed-hook-runtime-node24.mjs",
"durationSeconds": 1,
"summary": "All six companions loaded under Node 24.20.0; Codex and Claude produced successful installations and executable hook scripts in an isolated temporary home."
}
],
"runtimeBudget": {
Expand All @@ -20348,12 +20348,12 @@
"evidence": "New deterministic artifact smoke with no timing oracle; CI history is not yet available."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "On main de4f272b31, the same Node 18 require exited 1 before exports with ERR_UNKNOWN_BUILTIN_MODULE for node:sqlite. The candidate passes all six loads and both installers; restoring the eager node:sqlite value import reproduces the original exit."
"status": "missing",
"evidence": "The runtime floor is now Node 24; the earlier Node 18 import regression no longer describes this contract."
},
"performanceBudget": {
"required": false,
"evidence": "The product change replaces one eager built-in import with a constructor-time built-in lookup. It adds no polling, IPC, filesystem scan, subprocess, listener, or recurring work."
"evidence": "The companion runtime-floor change adds no polling, IPC, filesystem scan, subprocess, listener, or recurring work."
},
"promotionCriteria": [
"Collect 100 consecutive focused CI passes or 14 days of soak history.",
Expand All @@ -20364,7 +20364,7 @@
"The artifact journey uses local filesystem calls rather than a live SSH transport.",
"Platform-named bundles are loaded on macOS locally; physical Linux and Windows evidence is not yet recorded."
],
"demotionRule": "Demote if any shipped companion stops loading on Node 18, selected installers mutate outside the isolated home, or the CI lane no longer runs the built artifact with Node 18."
"demotionRule": "Demote if any shipped companion stops loading on Node 24, selected installers mutate outside the isolated home, or the CI lane no longer runs the built artifact with Node 24."
},
{
"id": "terminal-history.daemon-checkpoint-session-isolation",
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/build-orcad-prebuilds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ async function build() {
const newer = findPostBaselineNodeApiNames(readFileSync(destination))
if (newer.length > 0) {
throw new Error(
`[orcad-prebuilds] ${slot}/${relative} imports ${newer.join(', ')}, above N-API ${SLOT_NAPI_VERSION}; host Node 18 could not load it`
`[orcad-prebuilds] ${slot}/${relative} imports ${newer.join(', ')}, above N-API ${SLOT_NAPI_VERSION}; the slot ABI would change`
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions config/scripts/build-orcad.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// Package the server separately from its compatibility launcher.
// Package the Node 24 server separately from its older-Node compatibility launcher.
import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import {
Expand Down Expand Up @@ -107,7 +107,7 @@ async function stageParcelWatcher(target) {
},
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(OUT_DIR, ORCAD_PARCEL_WATCHER_ENTRY),
external: ['./watcher.node'],
Expand Down Expand Up @@ -198,7 +198,7 @@ function buildForkedChild(entryPoint, outfile) {
entryPoints: [entryPoint],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile,
external: ORCAD_EXTERNAL_MODULES,
Expand Down
18 changes: 9 additions & 9 deletions config/scripts/build-relay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function buildRelayBundles(outDir) {
alias: JSONC_PARSER_ESM_ALIAS,
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'relay.js'),
// Native addons cannot be bundled — they must exist on the remote host.
Expand All @@ -123,7 +123,7 @@ async function buildRelayBundles(outDir) {
entryPoints: [WATCHER_ENTRY],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'relay-watcher.js'),
external: ['@parcel/watcher'],
Expand All @@ -138,7 +138,7 @@ async function buildRelayBundles(outDir) {
entryPoints: [AI_VAULT_SERVICE_ENTRY],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'relay-ai-vault-service.js'),
external: ['electron'],
Expand All @@ -153,7 +153,7 @@ async function buildRelayBundles(outDir) {
entryPoints: [OPENCODE_SQLITE_READER_ENTRY],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, RELAY_OPENCODE_SQLITE_READER_FILENAME),
external: ['electron'],
Expand All @@ -168,7 +168,7 @@ async function buildRelayBundles(outDir) {
entryPoints: [WSL_TRANSCRIPT_FS_PROCESS_ENTRY],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'wsl-transcript-fs-process-entry.js'),
external: ['electron'],
Expand All @@ -183,7 +183,7 @@ async function buildRelayBundles(outDir) {
entryPoints: [MANAGED_HOOK_RUNTIME_ENTRY],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'managed-hook-runtime.js'),
// Why: jsonc-parser's default UMD build keeps relative dynamic requires
Expand Down Expand Up @@ -298,7 +298,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) {
alias: JSONC_PARSER_ESM_ALIAS,
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'wsl-agent-hook-relay.js'),
sourcemap: false,
Expand All @@ -316,7 +316,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) {
entryPoints: [wslBrowserNetworkEntry],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, 'wsl-browser-network-relay.js'),
sourcemap: false,
Expand All @@ -338,7 +338,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) {
entryPoints: [join(ROOT, 'src/main/claude-accounts/claude-profile-wsl-entry.ts')],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile: join(outDir, WSL_CLAUDE_PROFILE_HELPER_FILENAME),
minify: true,
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/orcad-entry-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function buildOrcadEntry(outfile) {
entryPoints: [join(root, ORCAD_ENTRY_POINT)],
bundle: true,
platform: 'node',
target: 'node18',
target: 'node24',
format: 'cjs',
outfile,
external: ORCAD_EXTERNAL_MODULES,
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/pr-auxiliary-preflight-admission.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const routes = [
['shell_contracts', 'src/main/pty/pty-manager.ts'],
['orcad_browser', 'src/main/orcad/orcad-browser-provider.ts'],
['cross-version-wire', 'src/shared/orchestration-rpc-contract.ts'],
['managed_hook_node18', 'src/shared/agent-hook-status.ts']
['managed_hook_node24', 'src/shared/agent-hook-status.ts']
]

function evaluate(expression, context) {
Expand Down
6 changes: 3 additions & 3 deletions config/scripts/pr-code-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const PR_CHECK_JOBS = [
'orcad_browser',
'mobile_web_app',
'cross-version-wire',
'managed_hook_node18',
'managed_hook_node24',
'package',
'package_windows'
]
Expand Down Expand Up @@ -243,7 +243,7 @@ const CROSS_VERSION_WIRE_PREFIXES = [
]

const MANAGED_HOOK_PREFIXES = [
'config/scripts/smoke-managed-hook-runtime-node18',
'config/scripts/smoke-managed-hook-runtime-node24',
'config/scripts/build-relay',
'src/relay/',
'src/shared/agent-hook',
Expand Down Expand Up @@ -536,7 +536,7 @@ function jobDetector(job) {
return changesMobileWebApp
case 'cross-version-wire':
return (files) => files.some((file) => matchesPrefix(file, CROSS_VERSION_WIRE_PREFIXES))
case 'managed_hook_node18':
case 'managed_hook_node24':
return (files) => files.some((file) => matchesPrefix(file, MANAGED_HOOK_PREFIXES))
case 'package':
return (files) => files.some(isLinuxPackagePath)
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/pr-code-change-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const expensiveJobs = [
'test',
'orcad_browser',
'cross-version-wire',
'managed_hook_node18',
'managed_hook_node24',
'package',
'package_windows'
]
Expand Down
20 changes: 10 additions & 10 deletions config/scripts/pr-workflow-parallelism.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -281,22 +281,22 @@ describe('PR workflow parallelism', () => {
expect(packageJson.scripts['build:release']).toContain('pnpm run build:web-from-renderer')
})

it('smokes managed-hook companions under their supported Node 18 runtime', () => {
const steps = workflow.jobs.managed_hook_node18.steps
it('smokes managed-hook companions under their supported Node 24 runtime', () => {
const steps = workflow.jobs.managed_hook_node24.steps
const installIndex = steps.findIndex(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const buildIndex = steps.findIndex((step) => step.run === 'pnpm run build:relay')
const node18Index = steps.findIndex(
(step) => step.uses === 'actions/setup-node@v6' && step.with['node-version'] === '18'
const node24Index = steps.findIndex(
(step) => step.uses === 'actions/setup-node@v6' && step.with['node-version'] === '24'
)
const smokeIndex = steps.findIndex(
(step) => step.run === 'node config/scripts/smoke-managed-hook-runtime-node18.mjs'
(step) => step.run === 'node config/scripts/smoke-managed-hook-runtime-node24.mjs'
)

expect(installIndex).toBeLessThan(buildIndex)
expect(buildIndex).toBeLessThan(node18Index)
expect(node18Index).toBeLessThan(smokeIndex)
expect(buildIndex).toBeLessThan(node24Index)
expect(node24Index).toBeLessThan(smokeIndex)
})

it('restores the pnpm store before dependency installation', () => {
Expand Down Expand Up @@ -574,15 +574,15 @@ describe('PR workflow parallelism', () => {
'orcad_browser',
'mobile_web_app',
'cross-version-wire',
'managed_hook_node18',
'managed_hook_node24',
'package',
'package_windows'
])
const verifyStep = workflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
expect(verifyStep.env.MANAGED_HOOK_NODE18).toBe('${{ needs.managed_hook_node18.result }}')
expect(verifyStep.run).toContain('"$MANAGED_HOOK_NODE18"')
expect(verifyStep.env.MANAGED_HOOK_NODE24).toBe('${{ needs.managed_hook_node24.result }}')
expect(verifyStep.run).toContain('"$MANAGED_HOOK_NODE24"')
// Why assert this one too: the browser provider test skips itself without
// ORCA_BROWSER_EXECUTABLE, so it only guards anything if verify actually reads it.
expect(verifyStep.env.ORCAD_BROWSER).toBe('${{ needs.orcad_browser.result }}')
Expand Down
Loading
Loading