Skip to content
Closed
Show file tree
Hide file tree
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 Apr 18, 2026
703a5e4
refactor(cli): type onboard session schema
cv Apr 18, 2026
8d68555
refactor(cli): centralize skipped-step metadata
cv Apr 18, 2026
a46e092
fix(prek): add Docker fallback for hadolint
cv Apr 18, 2026
4ccd413
test(cli): add onboarding resume checkpoint harness
cv Apr 18, 2026
cab6980
refactor(cli): add in-memory onboarding driver
cv Apr 18, 2026
42d5bd2
refactor(cli): use canonical flow state for resume gating
cv Apr 18, 2026
d099e5b
test(e2e): align onboard resume script with messaging step
cv Apr 18, 2026
e4ad3a0
Merge remote-tracking branch 'origin/main' into refactor/onboarding-fsm
cv Apr 18, 2026
f72db94
refactor(cli): route onboard progress through persistent driver
cv Apr 18, 2026
e2ce0cb
refactor(cli): remove legacy onboarding step recorder
cv Apr 18, 2026
0238b27
refactor(cli): extract onboarding bootstrap helpers
cv Apr 18, 2026
66aaf2f
refactor(cli): extract onboarding inference and sandbox flows
cv Apr 18, 2026
fec12f6
refactor(cli): extract runtime and policy onboarding flows
cv Apr 18, 2026
1830d81
refactor(cli): extract host onboarding flow
cv Apr 18, 2026
cdf2670
test(cli): attribute onboarding helper coverage to dist
cv Apr 18, 2026
c48ea14
refactor(cli): extract onboarding run context
cv Apr 18, 2026
810ceb8
refactor(cli): extract onboarding orchestrator
cv Apr 18, 2026
7cdf3f8
refactor(cli): extract onboarding orchestrator deps
cv Apr 18, 2026
634e723
refactor(cli): extract onboarding shell helpers
cv Apr 18, 2026
6be71fe
refactor(cli): extract onboarding entry shell
cv Apr 18, 2026
0bc287b
refactor(cli): extract onboarding request and remediation helpers
cv Apr 18, 2026
8c70530
refactor(cli): extract onboarding dashboard helpers
cv Apr 18, 2026
cf60b61
refactor(cli): extract onboarding dashboard helpers
cv Apr 18, 2026
3cd90ea
refactor(cli): extract onboarding openshell helpers
cv Apr 18, 2026
9287b75
chore(skills): drop unrelated troubleshooting drift
cv Apr 18, 2026
fac7884
refactor(cli): extract onboarding gateway runtime helpers
cv Apr 18, 2026
2ea3206
refactor(cli): address coderabbit onboarding feedback
cv Apr 18, 2026
8315a9d
fix(cli): address latest coderabbit onboarding feedback
cv Apr 18, 2026
52596a1
refactor(cli): extract onboarding sandbox name helper
cv Apr 18, 2026
c0c61f9
refactor(cli): extract onboarding telegram and policy suggestion helpers
cv Apr 18, 2026
28fc4a1
Merge branch 'main' into refactor/onboarding-fsm
cv Apr 18, 2026
e9b163f
refactor(cli): extract onboarding messaging helper
cv Apr 18, 2026
965b0ed
refactor(cli): move onboarding preflight and inference setup
cv Apr 18, 2026
e9bbf47
refactor(cli): move onboarding policy selection ui
cv Apr 18, 2026
e217cec
refactor(cli): move onboarding sandbox creation
cv Apr 18, 2026
7f2f12b
refactor(cli): move onboarding inference validation helpers
cv Apr 18, 2026
22e0633
refactor(cli): move onboarding web search and build config
cv Apr 18, 2026
1e9ed9d
refactor(cli): move onboarding gateway and ollama helpers
cv Apr 18, 2026
d8f5000
refactor(cli): move onboarding ollama and gateway probe helpers
cv Apr 18, 2026
bab5d2d
refactor(cli): move onboarding provider management helpers
cv Apr 18, 2026
66ea9b8
refactor(cli): move onboarding openshell version helpers
cv Apr 19, 2026
c956819
refactor(cli): move onboarding runtime support helpers
cv Apr 19, 2026
5b02576
refactor(cli): move onboarding openclaw and provider config
cv Apr 19, 2026
57fa4dd
refactor(cli): move onboarding step wrapper composition
cv Apr 19, 2026
eb41086
refactor(cli): move onboarding dashboard and policy ui wrappers
cv Apr 19, 2026
f41dd07
merge: bring origin/main into onboarding fsm refactor
cv Apr 19, 2026
3da7b8d
fix(cli): address onboarding review follow-ups
cv Apr 20, 2026
7e8f662
test(cli): fix orchestrator test types
cv Apr 20, 2026
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ repos:
hooks:
- id: hadolint
name: hadolint
entry: hadolint
entry: scripts/run-hadolint.sh
language: system
files: (Dockerfile[^/]*|.*\.dockerfile)$
types: [file]
Expand Down
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

# Contributing to NVIDIA NemoClaw

Thank you for your interest in contributing to NVIDIA NemoClaw. This guide covers how to set up your development environment, run tests, and submit changes.
Expand All @@ -18,7 +21,7 @@ Install the following before you begin.
- Python 3.11+ (for blueprint and documentation builds)
- Docker (running)
- [uv](https://docs.astral.sh/uv/) (for Python dependency management)
- [hadolint](https://github.com/hadolint/hadolint) (Dockerfile linter — `brew install hadolint` on macOS)
- [hadolint](https://github.com/hadolint/hadolint) (Dockerfile linter — either install it locally, e.g. `brew install hadolint` on macOS, or keep Docker available for the pinned container fallback)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Getting Started

Expand Down Expand Up @@ -92,6 +95,8 @@ All git hooks are managed by [prek](https://prek.j178.dev/), a fast, single-bina

For a full manual check: `npx prek run --all-files`. For scoped runs: `npx prek run --from-ref <base> --to-ref HEAD`.

If `hadolint` is not on your `PATH`, the local `hadolint` hook falls back to the pinned `hadolint/hadolint:v2.14.0` Docker image automatically. Dockerfile linting is still required, so contributors must have either a local `hadolint` binary or a working Docker daemon.

If you still have `core.hooksPath` set from an old Husky setup, Git will ignore `.git/hooks`. Run `git config --unset core.hooksPath` in this repo, then `npm install` so `prek install` (via `prepare`) can register the hooks.

`make check` remains the primary documented linter entry point.
Expand Down
39 changes: 39 additions & 0 deletions scripts/run-hadolint.sh
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 "$@"
12 changes: 12 additions & 0 deletions src/lib/ansi-utils.ts
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, "");
}
149 changes: 149 additions & 0 deletions src/lib/onboard-bootstrap.test.ts
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();
});
});
100 changes: 100 additions & 0 deletions src/lib/onboard-bootstrap.ts
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,
},
};
}
Loading
Loading