diff --git a/fixtures/mvp/evidence/mvp-runtime-plan.md b/fixtures/mvp/evidence/mvp-runtime-plan.md
new file mode 100644
index 00000000..140aa9bf
--- /dev/null
+++ b/fixtures/mvp/evidence/mvp-runtime-plan.md
@@ -0,0 +1,8 @@
+# HWLAB L6 MVP Runtime Plan
+
+This evidence fixture is a mock artifact used by the CLI and cloud web skeleton.
+
+- Environment: dev
+- Endpoint: http://74.48.78.17:6667
+- Scope: browser/CLI/gateway through cloud-web/cloud-api
+- Status: dry-run only
diff --git a/fixtures/mvp/runtime.json b/fixtures/mvp/runtime.json
new file mode 100644
index 00000000..46992bd6
--- /dev/null
+++ b/fixtures/mvp/runtime.json
@@ -0,0 +1,85 @@
+{
+ "project": {
+ "projectId": "proj_mvp-l6",
+ "name": "HWLAB L6 MVP",
+ "description": "Mock project for the HWLAB CLI and cloud web skeleton.",
+ "status": "active",
+ "environment": "dev",
+ "labels": {
+ "track": "mvp",
+ "surface": "cli-web"
+ },
+ "createdBy": "service_hwlab-cli",
+ "createdAt": "2026-05-21T00:00:00.000Z",
+ "updatedAt": "2026-05-21T00:00:00.000Z"
+ },
+ "projects": [
+ {
+ "projectId": "proj_mvp-l6",
+ "name": "HWLAB L6 MVP",
+ "status": "active",
+ "environment": "dev",
+ "updatedAt": "2026-05-21T00:00:00.000Z"
+ },
+ {
+ "projectId": "proj_cloud-web",
+ "name": "Cloud Web Skeleton",
+ "status": "active",
+ "environment": "dev",
+ "updatedAt": "2026-05-20T12:00:00.000Z"
+ }
+ ],
+ "serviceRoute": [
+ "browser/CLI/gateway",
+ "master hwlab-edge-proxy",
+ "frp",
+ "D601 hwlab-dev/hwlab-router",
+ "cloud-web/cloud-api"
+ ],
+ "mvpSteps": [
+ "browser/CLI/gateway",
+ "master hwlab-edge-proxy",
+ "frp",
+ "D601 hwlab-dev/hwlab-router",
+ "cloud-web/cloud-api",
+ "hardware trusted closed loop",
+ "agent automation closed loop",
+ "evidence record",
+ "worker cleanup"
+ ],
+ "endpoints": {
+ "dev": "http://74.48.78.17:6667"
+ },
+ "loops": [
+ {
+ "name": "hardware trusted closed loop",
+ "description": "Route a validated wiring config to the gateway and observe the corresponding hardware operation lifecycle."
+ },
+ {
+ "name": "agent automation closed loop",
+ "description": "Schedule agent and worker sessions, execute a scoped operation, and persist the resulting audit and evidence trail."
+ }
+ ],
+ "evidence": [
+ {
+ "evidenceId": "evidence_mvp-runtime-plan",
+ "projectId": "proj_mvp-l6",
+ "operationId": "op_mvp-0001",
+ "kind": "report",
+ "uri": "fixtures/mvp/evidence/mvp-runtime-plan.md",
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "serviceId": "hwlab-cli",
+ "environment": "dev",
+ "createdAt": "2026-05-21T00:00:00.000Z",
+ "metadata": {
+ "source": "mock"
+ }
+ }
+ ],
+ "cleanup": [
+ "release worker sessions",
+ "clear transient gateway state",
+ "archive evidence references",
+ "close the CLI-runner task"
+ ]
+}
diff --git a/package.json b/package.json
index 5aa4d508..ad2dad1c 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,11 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs",
- "check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs"
+ "check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs",
+ "cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
+ "cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
+ "cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
+ "web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
+ "web:build": "node web/hwlab-cloud-web/scripts/build.mjs"
}
}
diff --git a/tools/hwlab-cli/.gitkeep b/tools/hwlab-cli/.gitkeep
deleted file mode 100644
index 8b137891..00000000
--- a/tools/hwlab-cli/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/tools/hwlab-cli/lib/cli.mjs b/tools/hwlab-cli/lib/cli.mjs
new file mode 100644
index 00000000..e65ec55a
--- /dev/null
+++ b/tools/hwlab-cli/lib/cli.mjs
@@ -0,0 +1,192 @@
+import fs from "node:fs";
+import path from "node:path";
+import { DEV_ENDPOINT } from "../../../internal/protocol/index.mjs";
+
+const DEFAULT_PROJECTS = [
+ {
+ projectId: "proj_mvp-l6",
+ name: "HWLAB L6 MVP",
+ status: "active",
+ environment: "dev",
+ updatedAt: "2026-05-21T00:00:00.000Z"
+ },
+ {
+ projectId: "proj_cloud-web",
+ name: "Cloud Web Skeleton",
+ status: "active",
+ environment: "dev",
+ updatedAt: "2026-05-20T12:00:00.000Z"
+ }
+];
+
+function loadRuntimeFixture(cwd) {
+ const fixturePath = path.resolve(cwd, "fixtures/mvp/runtime.json");
+ if (!fs.existsSync(fixturePath)) {
+ return null;
+ }
+ const raw = fs.readFileSync(fixturePath, "utf8");
+ return JSON.parse(raw);
+}
+
+function resolveRuntime(cwd) {
+ const fixture = loadRuntimeFixture(cwd);
+ if (fixture) {
+ return fixture;
+ }
+ return {
+ endpoints: { dev: DEV_ENDPOINT },
+ projects: DEFAULT_PROJECTS,
+ mvpSteps: [
+ "browser/CLI/gateway",
+ "master hwlab-edge-proxy",
+ "frp",
+ "D601 hwlab-dev/hwlab-router",
+ "cloud-web/cloud-api",
+ "hardware trusted closed loop",
+ "agent automation closed loop",
+ "evidence record",
+ "worker cleanup"
+ ],
+ loops: [
+ { name: "hardware trusted closed loop" },
+ { name: "agent automation closed loop" }
+ ],
+ evidence: [],
+ cleanup: []
+ };
+}
+
+function formatJson(value) {
+ return JSON.stringify(value, null, 2);
+}
+
+function writeLine(stream, line = "") {
+ stream.write(`${line}\n`);
+}
+
+function writeProjectList(runtime, stdout) {
+ writeLine(stdout, "hwlab-cli project list");
+ writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
+ for (const project of runtime.projects ?? []) {
+ writeLine(
+ stdout,
+ `${project.projectId} | ${project.name} | ${project.status} | ${project.environment} | ${project.updatedAt}`
+ );
+ }
+}
+
+function writeHealth(runtime, stdout) {
+ writeLine(stdout, "hwlab-cli health");
+ writeLine(stdout, "status: ok");
+ writeLine(stdout, `environment: dev`);
+ writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
+ writeLine(stdout, `projects: ${(runtime.projects ?? []).length}`);
+}
+
+function writeDryRun(runtime, stdout) {
+ writeLine(stdout, "hwlab-cli test e2e --env dev --mvp --dry-run");
+ writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
+ writeLine(stdout, "mvp route:");
+ writeLine(
+ stdout,
+ ` ${runtime.serviceRoute?.join(" -> ") ?? "browser/CLI/gateway -> master hwlab-edge-proxy -> frp -> D601 hwlab-dev/hwlab-router -> cloud-web/cloud-api"}`
+ );
+ for (const step of runtime.mvpSteps ?? []) {
+ writeLine(stdout, `- ${step}`);
+ }
+ writeLine(stdout, "closed loops:");
+ for (const loop of runtime.loops ?? []) {
+ writeLine(stdout, `- ${loop.name}${loop.description ? `: ${loop.description}` : ""}`);
+ }
+ writeLine(stdout, "evidence record:");
+ for (const evidence of runtime.evidence ?? []) {
+ writeLine(
+ stdout,
+ `- ${evidence.evidenceId} | ${evidence.kind} | ${evidence.serviceId} | ${evidence.uri}`
+ );
+ }
+ writeLine(stdout, "worker cleanup:");
+ for (const step of runtime.cleanup ?? []) {
+ writeLine(stdout, `- ${step}`);
+ }
+ writeLine(stdout, "dry-run only: no DEV/PROD changes were made");
+}
+
+function writeHelp(stdout) {
+ writeLine(stdout, "hwlab-cli");
+ writeLine(stdout, "commands:");
+ writeLine(stdout, " health");
+ writeLine(stdout, " project list");
+ writeLine(stdout, " test e2e --env dev --mvp --dry-run");
+}
+
+function parseArgs(args) {
+ const flags = new Set();
+ const options = new Map();
+ const rest = [];
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+ if (arg.startsWith("--")) {
+ flags.add(arg);
+ const next = args[index + 1];
+ if (next && !next.startsWith("--")) {
+ options.set(arg, next);
+ index += 1;
+ }
+ } else {
+ rest.push(arg);
+ }
+ }
+ return { flags, options, rest };
+}
+
+export async function runCli(argv, io) {
+ const stdout = io.stdout ?? process.stdout;
+ const stderr = io.stderr ?? process.stderr;
+ const runtime = resolveRuntime(io.cwd ?? process.cwd());
+ const [command = "help", subcommand, ...rest] = argv;
+ const { flags, options } = parseArgs(rest);
+
+ if (command === "health") {
+ writeHealth(runtime, stdout);
+ return 0;
+ }
+
+ if (command === "project" && subcommand === "list") {
+ writeProjectList(runtime, stdout);
+ return 0;
+ }
+
+ if (command === "test" && subcommand === "e2e") {
+ const env = options.get("--env") ?? null;
+ const isDryRun = flags.has("--dry-run");
+ const isMvp = flags.has("--mvp");
+ if (env !== "dev") {
+ writeLine(stderr, "only --env dev is supported in the skeleton");
+ return 1;
+ }
+ if (!isMvp || !isDryRun) {
+ writeLine(stderr, "use --mvp --dry-run for the skeleton");
+ return 1;
+ }
+ writeDryRun(runtime, stdout);
+ return 0;
+ }
+
+ if (command === "project" && !subcommand) {
+ writeLine(stderr, "usage: hwlab-cli project list");
+ return 1;
+ }
+
+ if (command === "test" && !subcommand) {
+ writeLine(stderr, "usage: hwlab-cli test e2e --env dev --mvp --dry-run");
+ return 1;
+ }
+
+ writeHelp(stdout);
+ return 0;
+}
+
+export function formatRuntime(runtime) {
+ return formatJson(runtime);
+}
diff --git a/tools/hwlab-cli/package.json b/tools/hwlab-cli/package.json
new file mode 100644
index 00000000..9fde11f8
--- /dev/null
+++ b/tools/hwlab-cli/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "hwlab-cli",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "bin": {
+ "hwlab-cli": "./bin/hwlab-cli.mjs"
+ },
+ "scripts": {
+ "check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs",
+ "health": "node bin/hwlab-cli.mjs health"
+ }
+}
diff --git a/web/hwlab-cloud-web/.gitignore b/web/hwlab-cloud-web/.gitignore
new file mode 100644
index 00000000..849ddff3
--- /dev/null
+++ b/web/hwlab-cloud-web/.gitignore
@@ -0,0 +1 @@
+dist/
diff --git a/web/hwlab-cloud-web/.gitkeep b/web/hwlab-cloud-web/.gitkeep
deleted file mode 100644
index 8b137891..00000000
--- a/web/hwlab-cloud-web/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs
new file mode 100644
index 00000000..01021424
--- /dev/null
+++ b/web/hwlab-cloud-web/app.mjs
@@ -0,0 +1,40 @@
+import { runtime } from "./runtime.mjs";
+
+const runtimeGrid = document.querySelector("#runtime-grid");
+const steps = document.querySelector("#mvp-steps");
+const projectsBody = document.querySelector("#projects-body");
+const status = document.querySelector("#status");
+
+status.textContent = "dev skeleton";
+
+const runtimePairs = [
+ ["Endpoint", runtime.endpoints.dev],
+ ["Route", runtime.serviceRoute.join(" -> ")],
+ ["Closed loops", runtime.closedLoops.join(" / ")],
+ ["Evidence", runtime.evidence[0].uri]
+];
+
+for (const [label, value] of runtimePairs) {
+ const dt = document.createElement("dt");
+ dt.textContent = label;
+ const dd = document.createElement("dd");
+ dd.textContent = value;
+ runtimeGrid.append(dt, dd);
+}
+
+for (const step of runtime.mvpSteps) {
+ const li = document.createElement("li");
+ li.textContent = step;
+ steps.append(li);
+}
+
+for (const project of runtime.projects) {
+ const row = document.createElement("tr");
+ row.innerHTML = `
+
${project.projectId} ${project.name} |
+ ${project.status} |
+ ${project.environment} |
+ ${project.updatedAt} |
+ `;
+ projectsBody.append(row);
+}
diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html
new file mode 100644
index 00000000..6f04d800
--- /dev/null
+++ b/web/hwlab-cloud-web/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ HWLAB Cloud Web
+
+
+
+
+
+
+
+
+
+
Runtime
+
Mock dashboard for the L6 CLI and cloud-web surface.
+
+
+
+
+
+
+
MVP route
+
Fixed dry-run path shared with the CLI skeleton.
+
+
+
+
+
+
+
Projects
+
Static project list seeded from the mock fixture.
+
+
+
+
+
+ | Project |
+ Status |
+ Environment |
+ Updated |
+
+
+
+
+
+
+
+
+
diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json
new file mode 100644
index 00000000..2871c36c
--- /dev/null
+++ b/web/hwlab-cloud-web/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "hwlab-cloud-web",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "check": "node scripts/check.mjs",
+ "build": "node scripts/build.mjs"
+ }
+}
diff --git a/web/hwlab-cloud-web/runtime.mjs b/web/hwlab-cloud-web/runtime.mjs
new file mode 100644
index 00000000..f56e840b
--- /dev/null
+++ b/web/hwlab-cloud-web/runtime.mjs
@@ -0,0 +1,51 @@
+export const runtime = {
+ endpoints: {
+ dev: "http://74.48.78.17:6667"
+ },
+ serviceRoute: [
+ "browser/CLI/gateway",
+ "master hwlab-edge-proxy",
+ "frp",
+ "D601 hwlab-dev/hwlab-router",
+ "cloud-web/cloud-api"
+ ],
+ mvpSteps: [
+ "browser/CLI/gateway",
+ "master hwlab-edge-proxy",
+ "frp",
+ "D601 hwlab-dev/hwlab-router",
+ "cloud-web/cloud-api",
+ "hardware trusted closed loop",
+ "agent automation closed loop",
+ "evidence record",
+ "worker cleanup"
+ ],
+ closedLoops: [
+ "hardware trusted closed loop",
+ "agent automation closed loop"
+ ],
+ projects: [
+ {
+ projectId: "proj_mvp-l6",
+ name: "HWLAB L6 MVP",
+ status: "active",
+ environment: "dev",
+ updatedAt: "2026-05-21T00:00:00.000Z"
+ },
+ {
+ projectId: "proj_cloud-web",
+ name: "Cloud Web Skeleton",
+ status: "active",
+ environment: "dev",
+ updatedAt: "2026-05-20T12:00:00.000Z"
+ }
+ ],
+ evidence: [
+ {
+ evidenceId: "evidence_mvp-runtime-plan",
+ kind: "report",
+ uri: "fixtures/mvp/evidence/mvp-runtime-plan.md",
+ serviceId: "hwlab-cli"
+ }
+ ]
+};
diff --git a/web/hwlab-cloud-web/scripts/build.mjs b/web/hwlab-cloud-web/scripts/build.mjs
new file mode 100644
index 00000000..8d8a057d
--- /dev/null
+++ b/web/hwlab-cloud-web/scripts/build.mjs
@@ -0,0 +1,12 @@
+import fs from "node:fs";
+import path from "node:path";
+
+const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
+const distDir = path.resolve(rootDir, "dist");
+fs.mkdirSync(distDir, { recursive: true });
+
+for (const file of ["index.html", "styles.css", "app.mjs", "runtime.mjs"]) {
+ fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
+}
+
+console.log(`hwlab-cloud-web build ok: ${distDir}`);
diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs
new file mode 100644
index 00000000..be93ed4f
--- /dev/null
+++ b/web/hwlab-cloud-web/scripts/check.mjs
@@ -0,0 +1,19 @@
+import fs from "node:fs";
+import path from "node:path";
+
+const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
+const requiredFiles = ["index.html", "styles.css", "app.mjs", "runtime.mjs"];
+
+for (const file of requiredFiles) {
+ const filePath = path.resolve(rootDir, file);
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`missing web asset: ${file}`);
+ }
+}
+
+const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
+if (!html.includes("HWLAB Cloud Web")) {
+ throw new Error("index.html missing title");
+}
+
+console.log("hwlab-cloud-web check ok");
diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css
new file mode 100644
index 00000000..1f8a2437
--- /dev/null
+++ b/web/hwlab-cloud-web/styles.css
@@ -0,0 +1,154 @@
+:root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --surface: #161b22;
+ --surface-2: #1f2630;
+ --text: #e6edf3;
+ --muted: #9aa4b2;
+ --accent: #58a6ff;
+ --border: #30363d;
+ --good: #3fb950;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+ background: var(--bg);
+ color: var(--text);
+ font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+body {
+ padding: 24px;
+}
+
+.shell {
+ max-width: 1160px;
+ margin: 0 auto;
+ display: grid;
+ gap: 16px;
+}
+
+.topbar,
+.panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+}
+
+.topbar {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: center;
+ padding: 16px 18px;
+}
+
+.eyebrow {
+ margin: 0 0 4px;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0;
+ font-size: 12px;
+}
+
+h1,
+h2,
+p,
+dl,
+ol,
+table {
+ margin: 0;
+}
+
+h1 {
+ font-size: 22px;
+}
+
+.status {
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ background: var(--surface-2);
+ color: var(--good);
+}
+
+.panel {
+ padding: 16px 18px;
+}
+
+.panel-head {
+ display: grid;
+ gap: 4px;
+ margin-bottom: 16px;
+}
+
+.panel-head p {
+ color: var(--muted);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px 18px;
+}
+
+.grid dt {
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.grid dd {
+ margin: 2px 0 0;
+ font-weight: 600;
+ word-break: break-word;
+}
+
+.steps {
+ display: grid;
+ gap: 8px;
+ padding-left: 20px;
+}
+
+.steps li {
+ padding-left: 8px;
+}
+
+.table-wrap {
+ overflow-x: auto;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+th,
+td {
+ text-align: left;
+ padding: 10px 8px;
+ border-bottom: 1px solid var(--border);
+}
+
+th {
+ color: var(--muted);
+ font-weight: 500;
+}
+
+@media (max-width: 760px) {
+ body {
+ padding: 14px;
+ }
+
+ .topbar {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .grid {
+ grid-template-columns: 1fr;
+ }
+}