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
23 changes: 13 additions & 10 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,12 @@ async function runRendererOnboardingSmoke(win: BrowserWindow): Promise<void> {
throw new Error('isolated packaged smoke should be locked by OPENALICE_HOME')
}

const setup = await json(await fetch('/api/workspaces/project-setup'))
const workspaceList = await json(await fetch('/api/workspaces'))
if (setup.pending?.length || !workspaceList.workspaces?.some(ws => ws.template === 'chat')) {
throw new Error('new project did not prepare Chat before opening the renderer')
}

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')
Expand All @@ -484,15 +490,15 @@ async function runRendererOnboardingSmoke(win: BrowserWindow): Promise<void> {
}, 60000)

if (!initialReadiness.agents.pi?.ready) {
await waitFor('AI credential action', () => {
const addCredential = await waitFor('AI credential action', () => {
const button = document.querySelector('[data-testid="first-run-guide-primary"]')
return button &&
!button.disabled &&
button.getAttribute('data-onboarding-action') === 'add-credential'
? true
: false
? button
: document.querySelector('[data-testid="first-run-guide-add-provider"]')
})
clickPrimary()
addCredential.click()
await waitFor('credential modal', () => credentialPrimary())
credentialPrimary().click()
await waitFor('verified credential', () => {
Expand All @@ -518,13 +524,10 @@ async function runRendererOnboardingSmoke(win: BrowserWindow): Promise<void> {
const snapshot = await json(await fetch('/api/agent-runtimes/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 &&
button.getAttribute('data-onboarding-action') === 'continue'
return row?.ready === true && (activeStep() === 'broker' || (activeStep() === 'ai' &&
button && !button.disabled && button.getAttribute('data-onboarding-action') === 'continue'))
}, 60000)
clickPrimary()
if (activeStep() === 'ai') clickPrimary()
await waitFor('broker step', () => activeStep() === 'broker' ? true : false)

return {
Expand Down
34 changes: 30 additions & 4 deletions docs/alice-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,31 @@ plus the matching Settings categories
Ask Alice or Settings. AutoQuant and Tracked stay visible until that
boundary is reviewed separately.

A new AliceProject with no Chat Workspace opens Ask Alice on the shared
harness setup page rather than an empty composer; Chat does not pin a Harness
version.
Named AliceProject creation selects the workspaces to prepare. Chat is the
default; Auto Quant and Auto Prediction are optional, and selecting none leaves
setup for later. The TUI Foundry adds a third Workspaces step (arrows to move,
Space/click to toggle) and finishes with Create & start. Scripted creation uses
`--workspaces chat,auto-quant,auto-prediction` or `--workspaces none`; it records
the selection without starting a Runtime. The next startup prepares it.

The CLI and TUI share the same registration and `workspace-setup.json` birth
request. Under its writer lease, the backend resolves or creates each selected
Workspace and saves the canonical Harness default before checkpointing success.
It never starts an Agent Session or copies credentials. An interrupted attempt
reuses the existing Workspace. Failed items remain pending, while successful
items are not recreated. Quick Start reports remaining setup and offers Retry;
the existing per-Harness setup page still supports deferred/legacy projects.
A missing request leaves older homes unchanged. Chat does not pin a Harness version.

A prepared Workspace may still need an Agent or credentials before its first
Session; the normal launch controls own that readiness, independently of setup.
The first-run guide offers an explicit Pi provider connection action. Saving a
compatible provider binds it to an unconfigured Chat's interactive defaults;
existing runtime choices and headless defaults are preserved. The guide stays
on the confirmed AI-ready step until the user continues. Its page layer sits
below the shared credential/UTA dialogs so these remain visible and operable.
Packaged onboarding smoke uses isolated Pi state, the local mock provider, and
the same Chat birth request before verifying the renderer and provider binding.

Create a named project from the CLI:

Expand All @@ -87,7 +109,11 @@ for AI credential rows: it copies only `credentials` from the per-home
`ai-provider-manager.json`, writes into the destination home, and never prints
secrets. Workspace launch preferences and broker credentials stay project-local.

The Supervisor TUI create path still registers a Trader-equivalent home.
The Supervisor TUI create path registers a Trader-equivalent home.

For isolated first-run verification, use
`OPENALICE_ONBOARDING_WORKSPACES=chat pnpm dev:onboarding`. Omit the variable
to exercise the legacy/deferred setup page. `none` explicitly skips preparation.

The application/source root is launch metadata, not identity. Ports and Web
URLs are live discovery data and may change between launches. Guardian's
Expand Down
9 changes: 7 additions & 2 deletions docs/cli-supervisor.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ intentionally parameter-free:
80-column baseline. Its Create row opens a two-stage AliceProject Foundry:
Identity and Complete Home remain visible beside the focused Field Inspector
on wide terminals and stack as a compact route at 80 columns. Validation
remains ordered, and only the final `Create & select` action registers the
remains ordered, and only the final `Create & start` action registers the
new complete home. AI vault copy is a separate command:
`openalice project copy-ai-creds`;
- `p` opens Setup for data home, browser port, update checks, and resolved
Expand Down Expand Up @@ -898,7 +898,12 @@ switches the live Supervisor view and records it as the next bare-start
default; it does not stop, move, copy, or delete another project. Creating an
AliceProject collects a validated lowercase key and separate complete home
inside the TUI, rejects equal or nested registered homes, and selects the new
entry atomically. An existing target must be empty or recognizable as an
entry atomically. The final Workspaces step defaults to Chat, allows optional
Auto Quant and Auto Prediction (or none), then starts the selected project.
The backend prepares those durable instances before the first page opens;
Agent Sessions remain stopped. Failed preparation can be retried from Quick
Start. CLI `create alice-project --workspaces` records the same selection
for the next start. See [[docs/alice-project.md]]. An existing target must be empty or recognizable as an
OpenAlice complete home; an unrelated non-empty directory is rejected. A new
target is created and canonicalized when registered, so a later missing
registered Home is never silently recreated. A bare TUI launch falls back to
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"src/server-control.mjs",
"src/ssh-connect.mjs",
"src/uninstall.mjs",
"src/update.mjs"
"src/update.mjs",
"src/project-workspaces.ts"
],
"scripts": {
"build": "tsc -p tsconfig.json --noEmit && node --check bin/openalice.ts && node --check bin/openalice.mjs && node --check src/alice-project.ts && node --check src/alice-project-product.ts && node --check src/ai-credential-copy.ts && node --check src/create-alice-project.ts && node --check src/project-command.ts && node --check src/project-transfer.ts && node --check src/project-transfer-files.ts && node --check src/project-transfer-secrets.ts && node --check src/project-transfer-ssh.ts && node --check src/project-transfer-stream.ts && node --check src/machine-command.ts && node --check src/machine-inventory.ts && node --check src/machine-registry.ts && node --check src/managed-source.ts && node --check src/supervisor-config.ts && node --check src/supervisor-boot-sequence.ts && node --check src/supervisor-connection-chronicle.ts && node --check src/supervisor-command-deck.ts && node --check src/supervisor-confirmation.ts && node --check src/supervisor-display.ts && node --check src/supervisor-doctor-view.ts && node --check src/supervisor-fleet.ts && node --check src/supervisor-help-view.ts && node --check src/supervisor-inbox.ts && node --check src/supervisor-launch-flight.ts && node --check src/supervisor-navigation.ts && node --check src/supervisor-transfer.ts && node --check src/supervisor-transfer-view.ts && node --check src/supervisor-overlay-pointer.ts && node --check src/supervisor-projects-view.ts && node --check src/supervisor-project-foundry-view.ts && node --check src/supervisor-source-view.ts && node --check src/supervisor-release-view.ts && node --check src/supervisor-task-surface.ts && node --check src/supervisor-scroll-rail.ts && node --check src/supervisor-setup-view.ts && node --check src/supervisor-tui-pointer.ts && node --check src/supervisor-tui-feedback.ts && node --check src/supervisor-tui-logs.ts && node --check src/supervisor-terminal-clipboard.ts && node --check src/supervisor-tui-theme.ts && node --check src/supervisor-tui-view.ts && node --check src/activation.mjs && node --check src/activation-runtime.mjs && node --check src/bun-standalone.mjs && node --check src/doctor.mjs && node --check src/install-layout.mjs && node --check src/install-source.mjs && node --check src/lifecycle-command.mjs && node --check src/lifecycle.mjs && node --check src/local-start.mjs && node --check src/logs.mjs && node --check src/observability-command.mjs && node --check src/package-manager.mjs && node --check src/remote.mjs && node --check src/runtime-deps.mjs && node --check src/runtime-bundle.mjs && node --check src/runtime-client.mjs && node --check src/server.mjs && node --check src/server-control.mjs && node --check src/ssh-connect.mjs && node --check src/uninstall.mjs && node --check src/update.mjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { runSupervisorTui } from '../supervisor-tui.ts'
// Real creation/registry and PTY input; lifecycle is isolated from network/process launches.
const code = await runSupervisorTui({}, {
env: process.env,
inspect: async () => ({ class: 'absent', state: 'absent', owner: null, endpoints: {} }),
start: async () => {},
open: async () => {},
})
process.exitCode = code
7 changes: 7 additions & 0 deletions packages/cli/src/create-alice-project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('openalice create alice-project', () => {
homeDir,
},
)).resolves.toBe(0)
expect(JSON.parse(await readFile(join(home, 'workspace-setup.json'), 'utf8')).pending).toEqual(['chat'])
expect(stdout.join('')).toContain('NanoAlice')
expect(stdout.join('')).toContain('openalice up --project office')
expect(JSON.parse(await readFile(aliceProjectProductStampPath(home), 'utf8'))).toEqual({
Expand All @@ -59,3 +60,9 @@ describe('openalice create alice-project', () => {
expect(saved.projects?.office?.product).toBe('nano')
})
})

it('parses explicit workspace selection and rejects typos', () => {
expect(parseCreateAliceProjectArgs(['--workspaces', 'chat,auto-quant,chat']).workspaces).toEqual(['chat', 'auto-quant'])
expect(parseCreateAliceProjectArgs(['--workspaces', 'none']).workspaces).toEqual([])
expect(() => parseCreateAliceProjectArgs(['--workspaces', 'quant'])).toThrow('Workspaces must')
})
13 changes: 13 additions & 0 deletions packages/cli/src/create-alice-project.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseProjectWorkspaces, type ProjectWorkspace } from './project-workspaces.ts'
/**
* `openalice create alice-project` — interactive or scripted AliceProject birth.
*/
Expand Down Expand Up @@ -33,6 +34,7 @@ Options:
--name <key> Project key (lowercase, not "default")
--home <path> Complete OPENALICE_HOME for this project
--product <kind> trader (default) or nano
--workspaces <list> chat (default), auto-quant, auto-prediction; or none
--yes Non-interactive; requires --name and --home
`
}
Expand All @@ -42,6 +44,7 @@ export interface CreateAliceProjectOptions {
home?: string
product?: AliceProjectProduct
yes?: boolean
workspaces?: ProjectWorkspace[]
}

export function parseCreateAliceProjectArgs(argv: string[]): CreateAliceProjectOptions {
Expand All @@ -52,6 +55,10 @@ export function parseCreateAliceProjectArgs(argv: string[]): CreateAliceProjectO
options.yes = true
continue
}
if (arg === '--workspaces') {
options.workspaces = parseProjectWorkspaces(requireValue(argv, ++index, arg))
continue
}
if (arg === '--name') {
options.name = requireValue(argv, ++index, arg)
continue
Expand Down Expand Up @@ -115,6 +122,10 @@ export async function runCreateAliceProjectCommand(
?? (interactive ? await prompt(`Complete home [${suggestedHome}]: `) : suggestedHome)
).trim() || suggestedHome

const workspaces = options.workspaces ?? (interactive
? parseProjectWorkspaces((await prompt('Workspaces: chat, auto-quant, auto-prediction, or none [chat]: ')).trim() || 'chat')
: ['chat'] as ProjectWorkspace[])

if (interactive && !options.yes) {
stdout.write(
`Create AliceProject "${name}" as ${product === 'nano' ? 'NanoAlice' : 'TraderAlice'} at ${home}?\n`,
Expand All @@ -129,12 +140,14 @@ export async function runCreateAliceProjectCommand(
const context = await (io.resolveContext ?? (() => resolveStoredLaunchContext({})))()
await createSupervisorAliceProject(context, name, home, {
product,
workspaces,
homeDir: io.homeDir,
cwd: home,
})
stdout.write(
`Created AliceProject ${name} (${product === 'nano' ? 'NanoAlice' : 'TraderAlice'}).\n`
+ `Home: ${home}\n`
+ `Workspaces: ${workspaces.join(', ') || 'none (set up later)'}. Prepared automatically on first start; no Agent is launched.\n`
+ `Selected as the next bare-start default. Start with: openalice up --project ${name}\n`,
)
return 0
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/project-workspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'

export const PROJECT_WORKSPACES = ['chat', 'auto-quant', 'auto-prediction'] as const
export type ProjectWorkspace = typeof PROJECT_WORKSPACES[number]
export const PROJECT_WORKSPACE_LABELS: Record<ProjectWorkspace, string> = {
chat: 'Chat — everyday conversations and tasks',
'auto-quant': 'Auto Quant — quantitative research',
'auto-prediction': 'Auto Prediction — prediction market research',
}

export function parseProjectWorkspaces(value: string): ProjectWorkspace[] {
if (value.trim() === 'none') return []
const values = value.split(',').map(item => item.trim())
if (values.some(item => !PROJECT_WORKSPACES.includes(item as ProjectWorkspace))) {
throw new Error('Workspaces must be chat, auto-quant, auto-prediction (comma-separated), or none.')
}
return [...new Set(values)] as ProjectWorkspace[]
}

/** Birth-time intent only. The owning backend prepares these without starting Agents. */
export async function writeProjectWorkspaceRequest(home: string, workspaces: readonly ProjectWorkspace[]): Promise<void> {
// Exclusive creation: never replace an existing home's setup choices.
await writeFile(join(home, 'workspace-setup.json'), JSON.stringify({
schemaVersion: 1, pending: workspaces,
}, null, 2) + '\n', { flag: 'wx', mode: 0o600 }).catch((error: NodeJS.ErrnoException) => {
if (error.code !== 'EEXIST') throw error
})
}
4 changes: 4 additions & 0 deletions packages/cli/src/supervisor-config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { writeProjectWorkspaceRequest, type ProjectWorkspace } from './project-workspaces.ts'
import { randomUUID } from 'node:crypto'
import { constants } from 'node:fs'
import {
Expand Down Expand Up @@ -391,6 +392,7 @@ export async function createSupervisorAliceProject(
home: string,
options: PersistAliceProjectConfigOptions & {
product?: AliceProjectProduct
workspaces?: readonly ProjectWorkspace[]
displayName?: string
select?: boolean
} = {},
Expand Down Expand Up @@ -443,6 +445,8 @@ export async function createSupervisorAliceProject(
},
}
await assertRegistryHomesSeparate(next, options)
// Registration also serves transfers: only new-project flows request setup.
if (options.workspaces !== undefined) await writeProjectWorkspaceRequest(normalizedHome, options.workspaces)
await writeConfig(context.supervisorRoot, next)
}

Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/supervisor-project-foundry-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ describe('Supervisor AliceProject Foundry', () => {
message: 'Existing data is never copied or deleted.',
}, 100)
const output = rendered.lines.join('\n')
expect(output).toContain('Foundry · 2/2 · COMPLETE HOME')
expect(output).toContain('Foundry · 2/3 · COMPLETE HOME')
expect(output).toContain('Create AliceProject · research')
expect(output).toContain('✓ 01 Identity')
expect(output).toContain('◆ 02 Complete Home')
expect(output).toContain('◆ [ Enter ] Create & select │ [ Esc ] Back')
expect(output).toContain('◆ [ Enter ] Continue │ [ Esc ] Back')
expect(output).toContain('Foundry contract · COMPLETE HOME')
expect(output).not.toContain('…')
expect(rendered.lines.every((line) => displayWidth(line) <= 100)).toBe(true)
Expand All @@ -39,8 +39,8 @@ describe('Supervisor AliceProject Foundry', () => {
message: 'Create a named AliceProject without leaving the Supervisor.',
}, 72)
const output = rendered.lines.join('\n')
expect(output).toContain('AliceProject Foundry · 1/2 · IDENTITY')
expect(output).toContain('◆ Identity → Complete Home')
expect(output).toContain('AliceProject Foundry · 1/3 · IDENTITY')
expect(output).toContain('◆ Identity → Home → Workspaces')
expect(output).toContain('Create AliceProject · Project key')
expect(output).toContain('◆ CONTRACT')
expect(rendered.lines).toHaveLength(14)
Expand Down
Loading