Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .github/workflows/desktop-package-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ on:
- "pnpm-lock.yaml"
- "scripts/assert-desktop-package.mjs"
- "scripts/desktop-packaged-smoke.mjs"
- "scripts/desktop-packaged-smoke-plan.mjs"
- "scripts/smoke-packaged-toolchain.mjs"
- "scripts/vendor-managed-runtime.mjs"
- "src/core/paths.ts"
- "src/core/runtime-profile.ts"
Expand All @@ -31,6 +33,8 @@ on:
- "pnpm-lock.yaml"
- "scripts/assert-desktop-package.mjs"
- "scripts/desktop-packaged-smoke.mjs"
- "scripts/desktop-packaged-smoke-plan.mjs"
- "scripts/smoke-packaged-toolchain.mjs"
- "scripts/vendor-managed-runtime.mjs"
- "src/core/paths.ts"
- "src/core/runtime-profile.ts"
Expand Down Expand Up @@ -90,3 +94,6 @@ jobs:

- name: Assert packaged resource layout
run: pnpm electron:assert-package

- name: Smoke packaged managed toolchain
run: pnpm electron:smoke-toolchain
112 changes: 111 additions & 1 deletion apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ let uta: ChildProcess | null = null
let alice: ChildProcess | null = null
let appQuitting = false
let restartingUTA = false
let rendererOnboardingSmokeStarted = false

const DEFAULT_WEB_PORT_START = 47331
const READY_TIMEOUT_MS = 30_000
Expand Down Expand Up @@ -362,6 +363,98 @@ async function runRendererPtySmoke(win: BrowserWindow): Promise<void> {
console.log(`[guardian] electron smoke pty → ok workspace=${result.workspaceId ?? ''} session=${result.sessionId ?? ''}`)
}

async function runRendererOnboardingSmoke(win: BrowserWindow): Promise<void> {
const result = await win.webContents.executeJavaScript(`(async () => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const json = async (res) => {
const text = await res.text()
let body = null
try { body = text ? JSON.parse(text) : null } catch { body = text }
if (!res.ok) throw new Error(res.status + ' ' + text)
return body
}
const waitFor = async (label, predicate, timeoutMs = 12000) => {
const deadline = Date.now() + timeoutMs
let last = null
while (Date.now() < deadline) {
try {
const value = await predicate()
if (value) return value
} catch (err) {
last = err
}
await sleep(100)
}
throw new Error('Timed out waiting for ' + label + (last ? ': ' + (last.message || String(last)) : ''))
}
const activeStep = () => document
.querySelector('[data-testid="first-run-guide-step"]')
?.getAttribute('data-onboarding-step') || null
const clickPrimary = () => {
const button = document.querySelector('[data-testid="first-run-guide-primary"]')
if (!button) throw new Error('first-run primary button missing')
button.click()
}

await waitFor('Electron preload bridge', () => Boolean(window.openAlice?.runtime && window.openAlice?.pty))

const agents = await json(await fetch('/api/workspaces/agents'))
const pi = agents.agents?.find((agent) => agent.id === 'pi')
if (!pi?.installed) throw new Error('managed Pi was not detected by packaged /agents')

const tradingStatus = await json(await fetch('/api/trading/status'))
if (tradingStatus.mode !== 'lite') {
throw new Error('expected fresh onboarding trading mode to be lite, got ' + tradingStatus.mode)
}

await waitFor('first-run guide', () => document.querySelector('[data-testid="first-run-guide"]'))
await waitFor('language step', () => activeStep() === 'language' ? true : false)
clickPrimary()
await waitFor('welcome step', () => activeStep() === 'lite' ? true : false)
clickPrimary()
await waitFor('AI access step', () => activeStep() === 'ai' ? true : false)

const readiness = await waitFor('Pi runtime readiness', async () => {
const snapshot = await json(await fetch('/api/workspaces/agent-runtime-readiness'))
const row = snapshot.agents?.pi
if (row?.ready && row.status === 'ready') return snapshot
if (row && row.status !== 'unknown' && row.status !== 'checking') {
throw new Error('Pi readiness was ' + row.status + ': ' + (row.message || 'no detail'))
}
return null
}, 60000)
const piReady = readiness.agents.pi

await waitFor('AI ready primary button', async () => {
const snapshot = await json(await fetch('/api/workspaces/agent-runtime-readiness'))
const row = snapshot.agents?.pi
const button = document.querySelector('[data-testid="first-run-guide-primary"]')
return activeStep() === 'ai' && row?.ready === true && button && !button.disabled
}, 60000)
clickPrimary()
await waitFor('broker step', () => activeStep() === 'broker' ? true : false)

return {
ok: true,
step: activeStep(),
piPath: pi.binPath || null,
runtimeStatus: piReady.status,
runtimeSource: piReady.source,
tradingMode: tradingStatus.mode,
}
})()`, true) as {
ok?: boolean
step?: string
piPath?: string | null
runtimeStatus?: string
runtimeSource?: string
tradingMode?: string
}
console.log(
`[guardian] electron smoke onboarding → ok step=${result.step ?? ''} mode=${result.tradingMode ?? ''} pi=${result.piPath ?? 'managed'} runtime=${result.runtimeStatus ?? ''}/${result.runtimeSource ?? ''}`,
)
}

app.whenReady().then(async () => {
// Build output lives at <repo>/dist/electron/main.js, <repo>/dist/main.js
// (Alice), and <repo>/services/uta/dist/uta.js (UTA). The desktop package
Expand Down Expand Up @@ -605,6 +698,20 @@ app.whenReady().then(async () => {
}
})
}
if (process.env['OPENALICE_ELECTRON_SMOKE_ONBOARDING'] === '1' && !rendererOnboardingSmokeStarted) {
rendererOnboardingSmokeStarted = true
void runRendererOnboardingSmoke(win)
.then(() => {
if (process.env['OPENALICE_ELECTRON_SMOKE_EXIT'] === '1') shutdown()
})
.catch((err) => {
console.error(`[guardian] electron smoke onboarding → failed: ${err instanceof Error ? err.message : String(err)}`)
if (process.env['OPENALICE_ELECTRON_SMOKE_EXIT'] === '1') {
process.exitCode = 1
shutdown()
}
})
}
})
.catch((err) => {
console.error(`[guardian] renderer bridge probe failed: ${err instanceof Error ? err.message : String(err)}`)
Expand Down Expand Up @@ -688,7 +795,10 @@ async function stopChildren(): Promise<void> {
/** Cascade tree-kill both children, then exit once they're gone. */
function shutdown(): void {
if (appQuitting) return
void stopChildren().finally(() => app.exit(0))
void stopChildren().finally(() => {
const exitCode = typeof process.exitCode === 'number' ? process.exitCode : 0
app.exit(exitCode)
})
}

app.on('before-quit', (e) => {
Expand Down
48 changes: 25 additions & 23 deletions docs/managed-workspace-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ Node runtime for the backend and workspace bootstrap.

- Packaged OpenAlice should include a managed Pi npm runtime on macOS and
Windows.
- Windows packaged OpenAlice should include exactly one managed Git+shell
runtime. Do not ship two Git trees.
- Windows packaged OpenAlice should include an incremental managed Git Bash
runtime. Dugite's embedded Git payload is retained in the first slice and
deduplicated only after the new runtime path is stable.
- The managed runtime must be discovered through explicit capability/profile
injection, not scattered `process.platform` guesses.
- Existing Workspace semantics stay intact: Pi is a coding runtime, not a
Expand All @@ -49,8 +50,7 @@ Node runtime for the backend and workspace bootstrap.
## Non-Goals

- Do not make Pi responsible for account/trading safety.
- Do not introduce a second Windows Git runtime beside the existing packaged
Git story.
- Do not remove or rewrite dugite in the first managed Git Bash slice.
- Do not require WSL, Git for Windows, Node, npm, pnpm, or system Git as a
first-run prerequisite.
- Do not rewrite every Git call in the first implementation if a smaller step
Expand Down Expand Up @@ -83,7 +83,7 @@ Ship:

- Electron Node (already present)
- managed Pi npm runtime (`@earendil-works/pi-coding-agent`)
- one managed Git for Windows / MinGit style runtime that includes:
- one managed Git for Windows / PortableGit runtime that includes:
- `git.exe`
- `bash.exe`
- `sh.exe`
Expand All @@ -93,16 +93,17 @@ Ship:
Use:

- managed `bash.exe` as Pi's `shellPath`
- the same managed Git runtime for workspace bootstrap, Git UI, and agent shell
commands
- the same managed Git runtime for agent shell commands and as the preferred
`LOCAL_GIT_DIRECTORY` for existing dugite call sites

Rationale:

- Pi's npm package does not provide a shell. OpenAlice must provide one.
- The existing dugite Windows native payload contains Git and POSIX-ish tools,
but not a reliable full Bash story.
- Shipping both dugite's embedded Git and another Git for Windows tree would be
wasteful and hard to reason about.
- Shipping both dugite's embedded Git and PortableGit is wasteful, but this
incremental slice accepts the temporary duplication so Windows gets a reliable
Bash/toolchain without rewriting launcher Git semantics in the same change.

## Runtime Profile

Expand Down Expand Up @@ -243,7 +244,7 @@ There are two separate concerns:
1. Git executable/runtime payload
2. JS API used by Alice code and bootstrap scripts

### Phase 1: Replace Windows Git Payload, Keep Dugite API
### Phase 1: Incremental Windows Git Bash, Keep Dugite API

Keep `dugite.exec()` call sites initially:

Expand All @@ -254,15 +255,16 @@ Keep `dugite.exec()` call sites initially:
But on Windows packaged builds:

- ship the managed Git for Windows runtime as `vendor/git/...`
- exclude or avoid packaging dugite's own `node_modules/dugite/git/**`
- set `LOCAL_GIT_DIRECTORY` to the managed Git dir before Alice starts
- keep dugite's own `node_modules/dugite/git/**` payload for now

Dugite supports `LOCAL_GIT_DIRECTORY`; with that env set, existing
`dugite.exec()` calls should resolve Git from the managed runtime instead of
dugite's embedded payload.

This gets us one Windows Git runtime while preserving the established API and
test surface.
This gives Windows a reliable Bash/toolchain while preserving the established
API and test surface. Having both Git payloads in the package is a known
temporary cost, not the final topology.

### Phase 2: OpenAlice Git Wrapper

Expand Down Expand Up @@ -321,15 +323,11 @@ Responsibilities:
- download pinned Pi install package + lockfile from the Pi release
- verify checksums
- run an isolated `npm ci --omit=dev` under `vendor/pi`
- emit a machine-readable manifest

Later responsibilities:

- download pinned Git for Windows / MinGit runtime for Windows
- download pinned Git for Windows / PortableGit runtime on Windows only
- verify checksums
- unpack into a deterministic vendor directory
- prune docs/examples only if license obligations remain satisfied
- extend the machine-readable manifest:
- emit a machine-readable manifest:

```json
{
Expand All @@ -340,9 +338,12 @@ Later responsibilities:
},
"git": {
"win32-x64": {
"version": "2.55.0.2",
"distribution": "PortableGit",
"path": "vendor/git/win32-x64",
"gitBin": "cmd/git.exe",
"shellPath": "bin/bash.exe"
"shellPath": "bin/bash.exe",
"shPath": "bin/sh.exe"
}
}
}
Expand Down Expand Up @@ -487,8 +488,9 @@ Windows acceptance smoke:
- Vendor one Windows Git+Shell runtime.
- Set `LOCAL_GIT_DIRECTORY`.
- Set managed `shellPath` to `bash.exe`.
- Exclude or avoid packaging dugite's embedded Windows Git payload.
- Confirm there is only one Git runtime in the Windows installer.
- Keep dugite's embedded Windows Git payload in this slice.
- Confirm PortableGit is packaged and the temporary dugite duplication is
visible in package assertions/docs.

### Step 5: Optional Dugite API Removal

Expand All @@ -498,7 +500,7 @@ Windows acceptance smoke:

## Open Questions

- Which Git for Windows / MinGit artifact should be pinned for Windows?
- When should the temporary Windows dugite + PortableGit duplication be removed?
- Can `LOCAL_GIT_DIRECTORY` fully cover every existing dugite call in packaged
Windows, or do any call sites depend on dugite-specific env setup?
- Should managed Pi be updated only with app releases, or should OpenAlice
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"electron:dev": "pnpm electron:build && pnpm -F @traderalice/desktop dev",
"electron:pack": "pnpm electron:build && pnpm vendor:runtime && pnpm -F @traderalice/desktop pack",
"electron:assert-package": "node scripts/assert-desktop-package.mjs",
"electron:smoke-toolchain": "node scripts/smoke-packaged-toolchain.mjs",
"electron:smoke:packaged": "node scripts/desktop-packaged-smoke.mjs",
"electron:smoke:onboarding": "node scripts/desktop-packaged-smoke.mjs --onboarding",
"electron:smoke:pty": "node scripts/desktop-pty-smoke.mjs",
"vendor:runtime": "node scripts/vendor-managed-runtime.mjs",
"test": "vitest run",
Expand Down
Loading
Loading