-
Notifications
You must be signed in to change notification settings - Fork 2.6k
refactor(cli): start onboarding fsm migration #2047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 28 commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
16b89ce
refactor(cli): add typed onboarding fsm core
cv 703a5e4
refactor(cli): type onboard session schema
cv 8d68555
refactor(cli): centralize skipped-step metadata
cv a46e092
fix(prek): add Docker fallback for hadolint
cv 4ccd413
test(cli): add onboarding resume checkpoint harness
cv cab6980
refactor(cli): add in-memory onboarding driver
cv 42d5bd2
refactor(cli): use canonical flow state for resume gating
cv d099e5b
test(e2e): align onboard resume script with messaging step
cv e4ad3a0
Merge remote-tracking branch 'origin/main' into refactor/onboarding-fsm
cv f72db94
refactor(cli): route onboard progress through persistent driver
cv e2ce0cb
refactor(cli): remove legacy onboarding step recorder
cv 0238b27
refactor(cli): extract onboarding bootstrap helpers
cv 66aaf2f
refactor(cli): extract onboarding inference and sandbox flows
cv fec12f6
refactor(cli): extract runtime and policy onboarding flows
cv 1830d81
refactor(cli): extract host onboarding flow
cv cdf2670
test(cli): attribute onboarding helper coverage to dist
cv c48ea14
refactor(cli): extract onboarding run context
cv 810ceb8
refactor(cli): extract onboarding orchestrator
cv 7cdf3f8
refactor(cli): extract onboarding orchestrator deps
cv 634e723
refactor(cli): extract onboarding shell helpers
cv 6be71fe
refactor(cli): extract onboarding entry shell
cv 0bc287b
refactor(cli): extract onboarding request and remediation helpers
cv 8c70530
refactor(cli): extract onboarding dashboard helpers
cv cf60b61
refactor(cli): extract onboarding dashboard helpers
cv 3cd90ea
refactor(cli): extract onboarding openshell helpers
cv 9287b75
chore(skills): drop unrelated troubleshooting drift
cv fac7884
refactor(cli): extract onboarding gateway runtime helpers
cv 2ea3206
refactor(cli): address coderabbit onboarding feedback
cv 8315a9d
fix(cli): address latest coderabbit onboarding feedback
cv 52596a1
refactor(cli): extract onboarding sandbox name helper
cv c0c61f9
refactor(cli): extract onboarding telegram and policy suggestion helpers
cv 28fc4a1
Merge branch 'main' into refactor/onboarding-fsm
cv e9b163f
refactor(cli): extract onboarding messaging helper
cv 965b0ed
refactor(cli): move onboarding preflight and inference setup
cv e9bbf47
refactor(cli): move onboarding policy selection ui
cv e217cec
refactor(cli): move onboarding sandbox creation
cv 7f2f12b
refactor(cli): move onboarding inference validation helpers
cv 22e0633
refactor(cli): move onboarding web search and build config
cv 1e9ed9d
refactor(cli): move onboarding gateway and ollama helpers
cv d8f5000
refactor(cli): move onboarding ollama and gateway probe helpers
cv bab5d2d
refactor(cli): move onboarding provider management helpers
cv 66ea9b8
refactor(cli): move onboarding openshell version helpers
cv c956819
refactor(cli): move onboarding runtime support helpers
cv 5b02576
refactor(cli): move onboarding openclaw and provider config
cv 57fa4dd
refactor(cli): move onboarding step wrapper composition
cv eb41086
refactor(cli): move onboarding dashboard and policy ui wrappers
cv f41dd07
merge: bring origin/main into onboarding fsm refactor
cv 3da7b8d
fix(cli): address onboarding review follow-ups
cv 7e8f662
test(cli): fix orchestrator test types
cv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #!/usr/bin/env bash | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| readonly HADOLINT_IMAGE="${HADOLINT_IMAGE:-hadolint/hadolint:v2.14.0}" | ||
|
|
||
| run_via_docker() { | ||
| if ! command -v docker >/dev/null 2>&1; then | ||
| printf '%s\n' "hadolint is not installed and Docker is unavailable." >&2 | ||
| printf '%s\n' "Install hadolint locally or make Docker available, then rerun prek." >&2 | ||
| return 127 | ||
| fi | ||
|
|
||
| if ! docker info >/dev/null 2>&1; then | ||
| printf '%s\n' "hadolint is not installed and Docker is not ready." >&2 | ||
| printf '%s\n' "Start Docker or install hadolint locally, then rerun prek." >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| printf '%s\n' "hadolint not found on PATH; linting Dockerfiles via Docker image ${HADOLINT_IMAGE}" >&2 | ||
|
|
||
| exec docker run --rm \ | ||
| -v "${PWD}:${PWD}" \ | ||
| -w "${PWD}" \ | ||
| "${HADOLINT_IMAGE}" \ | ||
| hadolint "$@" | ||
| } | ||
|
|
||
| main() { | ||
| if command -v hadolint >/dev/null 2>&1; then | ||
| exec hadolint "$@" | ||
| fi | ||
|
|
||
| run_via_docker "$@" | ||
| } | ||
|
|
||
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /** | ||
| * Strip ANSI escape sequences from terminal-oriented output. | ||
| * Covers CSI (color, erase, cursor), OSC, and C1 two-byte escapes per ECMA-48. | ||
| */ | ||
| export const ANSI_RE = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)|[@-_])/g; | ||
|
|
||
| export function stripAnsi(value: string): string { | ||
| return String(value || "").replace(ANSI_RE, ""); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import fs from "node:fs"; | ||
| import { createRequire } from "node:module"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it } from "vitest"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const bootstrapDistPath = require.resolve("../../dist/lib/onboard-bootstrap"); | ||
| const persistentDriverDistPath = require.resolve("../../dist/lib/onboard-persistent-driver"); | ||
| const flowStateDistPath = require.resolve("../../dist/lib/onboard-flow-state"); | ||
| const sessionDistPath = require.resolve("../../dist/lib/onboard-session"); | ||
| const originalHome = process.env.HOME; | ||
| let tmpDir: string; | ||
|
|
||
| beforeEach(() => { | ||
| tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-bootstrap-")); | ||
| process.env.HOME = tmpDir; | ||
| delete require.cache[bootstrapDistPath]; | ||
| delete require.cache[persistentDriverDistPath]; | ||
| delete require.cache[flowStateDistPath]; | ||
| delete require.cache[sessionDistPath]; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| delete require.cache[bootstrapDistPath]; | ||
| delete require.cache[persistentDriverDistPath]; | ||
| delete require.cache[flowStateDistPath]; | ||
| delete require.cache[sessionDistPath]; | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| if (originalHome === undefined) { | ||
| delete process.env.HOME; | ||
| } else { | ||
| process.env.HOME = originalHome; | ||
| } | ||
| }); | ||
|
|
||
| describe("initializeOnboardRun", () => { | ||
| it("creates a fresh session and resolves --from paths", () => { | ||
| const { initializeOnboardRun } = require("../../dist/lib/onboard-bootstrap"); | ||
|
|
||
| const result = initializeOnboardRun({ | ||
| resume: false, | ||
| mode: "non-interactive", | ||
| requestedFromDockerfile: "./Dockerfile.custom", | ||
| requestedAgent: "hermes", | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| if (!result.ok) { | ||
| throw new Error("expected fresh onboarding initialization to succeed"); | ||
| } | ||
| expect(result.value.session.mode).toBe("non-interactive"); | ||
| expect(result.value.session.agent).toBe("hermes"); | ||
| expect(result.value.fromDockerfile).toBe(path.resolve("./Dockerfile.custom")); | ||
| expect(result.value.driver.session?.metadata.fromDockerfile).toBe( | ||
| path.resolve("./Dockerfile.custom"), | ||
| ); | ||
| }); | ||
|
|
||
| it("returns a friendly error when no resumable session exists", () => { | ||
| const { initializeOnboardRun } = require("../../dist/lib/onboard-bootstrap"); | ||
|
|
||
| const result = initializeOnboardRun({ | ||
| resume: true, | ||
| mode: "interactive", | ||
| requestedFromDockerfile: null, | ||
| requestedAgent: null, | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| ok: false, | ||
| lines: [" No resumable onboarding session was found.", " Run: nemoclaw onboard"], | ||
| }); | ||
| }); | ||
|
|
||
| it("reports resume conflicts using the shared formatter", () => { | ||
| const onboardSession = require("../../dist/lib/onboard-session"); | ||
| const { initializeOnboardRun } = require("../../dist/lib/onboard-bootstrap"); | ||
|
|
||
| onboardSession.saveSession( | ||
| onboardSession.createSession({ | ||
| sandboxName: "alpha", | ||
| provider: "nvidia-prod", | ||
| model: "meta/llama-3.3-70b-instruct", | ||
| }), | ||
| ); | ||
|
|
||
| const result = initializeOnboardRun({ | ||
| resume: true, | ||
| mode: "interactive", | ||
| requestedFromDockerfile: null, | ||
| requestedAgent: null, | ||
| getResumeConflicts: (session: { sandboxName: string | null; provider: string | null }) => [ | ||
| { field: "sandbox", requested: "beta", recorded: session.sandboxName }, | ||
| { field: "provider", requested: "openai-api", recorded: session.provider }, | ||
| ], | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| ok: false, | ||
| lines: [ | ||
| " Resumable state belongs to sandbox 'alpha', not 'beta'.", | ||
| " Resumable state recorded provider 'nvidia-prod', not 'openai-api'.", | ||
| " Run: nemoclaw onboard # start a fresh onboarding session", | ||
| " Or rerun with the original settings to continue that session.", | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it("loads a resumable session, reuses the recorded Dockerfile, and clears failure state", () => { | ||
| const onboardSession = require("../../dist/lib/onboard-session"); | ||
| const { initializeOnboardRun } = require("../../dist/lib/onboard-bootstrap"); | ||
|
|
||
| onboardSession.saveSession( | ||
| onboardSession.createSession({ | ||
| mode: "interactive", | ||
| status: "failed", | ||
| sandboxName: "alpha", | ||
| metadata: { gatewayName: "nemoclaw", fromDockerfile: "/tmp/Recorded.Dockerfile" }, | ||
| failure: { | ||
| step: "policies", | ||
| message: "policy apply failed", | ||
| recordedAt: "2026-04-17T00:00:00.000Z", | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| const result = initializeOnboardRun({ | ||
| resume: true, | ||
| mode: "non-interactive", | ||
| requestedFromDockerfile: null, | ||
| requestedAgent: null, | ||
| getResumeConflicts: () => [], | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| if (!result.ok) { | ||
| throw new Error("expected resume initialization to succeed"); | ||
| } | ||
| expect(result.value.fromDockerfile).toBe("/tmp/Recorded.Dockerfile"); | ||
| expect(result.value.session.mode).toBe("non-interactive"); | ||
| expect(result.value.session.status).toBe("in_progress"); | ||
| expect(result.value.session.failure).toBeNull(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import path from "node:path"; | ||
|
|
||
| import { PersistentOnboardDriver } from "./onboard-persistent-driver"; | ||
| import { buildResumeConflictLines, type ResumeConfigConflict } from "./onboard-resume"; | ||
| import { createSession, type Session } from "./onboard-session"; | ||
|
|
||
| export interface InitializeOnboardRunOptions { | ||
| resume: boolean; | ||
| mode: Session["mode"]; | ||
| requestedFromDockerfile: string | null; | ||
| requestedAgent: string | null; | ||
| getResumeConflicts?: (session: Session) => ResumeConfigConflict[]; | ||
| } | ||
|
|
||
| export interface InitializedOnboardRun { | ||
| driver: PersistentOnboardDriver; | ||
| session: Session; | ||
| fromDockerfile: string | null; | ||
| } | ||
|
|
||
| export interface InitializeOnboardRunFailure { | ||
| ok: false; | ||
| lines: string[]; | ||
| } | ||
|
|
||
| export interface InitializeOnboardRunSuccess { | ||
| ok: true; | ||
| value: InitializedOnboardRun; | ||
| } | ||
|
|
||
| export type InitializeOnboardRunResult = | ||
| | InitializeOnboardRunFailure | ||
| | InitializeOnboardRunSuccess; | ||
|
|
||
| export function initializeOnboardRun( | ||
| options: InitializeOnboardRunOptions, | ||
| ): InitializeOnboardRunResult { | ||
| const driver = new PersistentOnboardDriver({ resume: options.resume }); | ||
|
|
||
| if (options.resume) { | ||
| const session = driver.session; | ||
| if (!session || session.resumable === false) { | ||
| return { | ||
| ok: false, | ||
| lines: [" No resumable onboarding session was found.", " Run: nemoclaw onboard"], | ||
| }; | ||
| } | ||
|
|
||
| const sessionFrom = session.metadata.fromDockerfile || null; | ||
| const fromDockerfile = options.requestedFromDockerfile | ||
| ? path.resolve(options.requestedFromDockerfile) | ||
| : sessionFrom | ||
| ? path.resolve(sessionFrom) | ||
| : null; | ||
| const resumeConflicts = options.getResumeConflicts?.(session) ?? []; | ||
| if (resumeConflicts.length > 0) { | ||
| return { | ||
| ok: false, | ||
| lines: buildResumeConflictLines(resumeConflicts), | ||
| }; | ||
| } | ||
|
|
||
| const updatedSession = driver.update((current) => { | ||
| current.mode = options.mode; | ||
| current.failure = null; | ||
| current.status = "in_progress"; | ||
| return current; | ||
| }); | ||
| return { | ||
| ok: true, | ||
| value: { | ||
| driver, | ||
| session: updatedSession, | ||
| fromDockerfile, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| const fromDockerfile = options.requestedFromDockerfile | ||
| ? path.resolve(options.requestedFromDockerfile) | ||
| : null; | ||
| const session = driver.replaceSession( | ||
| createSession({ | ||
| mode: options.mode, | ||
| agent: options.requestedAgent, | ||
| metadata: { gatewayName: "nemoclaw", fromDockerfile: fromDockerfile || null }, | ||
| }), | ||
| ); | ||
| return { | ||
| ok: true, | ||
| value: { | ||
| driver, | ||
| session, | ||
| fromDockerfile, | ||
| }, | ||
| }; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.