feat: add L6 CLI web MVP gate entry

This commit is contained in:
HWLAB Code Queue
2026-05-21 18:15:41 +00:00
parent f1fb088db6
commit 38d1507e07
16 changed files with 1984 additions and 245 deletions
+18
View File
@@ -67,3 +67,21 @@ Run the lightweight validation suite:
```sh
npm run check
```
## L6 Manual Acceptance Entry
The external MVP acceptance surface is DEV-only and report-driven:
```sh
npm run l6:smoke
npm run web:check
npm run cli:dry-run
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
The CLI command defaults to a JSON dry-run plan sourced from
`reports/dev-gate/dev-mvp-gate-report.json` and
`fixtures/mvp/m5-e2e/dry-run-plan.json`. A live DEV attempt requires
`--live --confirm-dev --confirmed-non-production`; with open gate blockers it
returns `BLOCKED` evidence rather than using fixtures or UniDesk runtime as a
substitute.
+21 -4
View File
@@ -2,7 +2,7 @@
M5 is the static orchestration gate before the first real DEV MVP e2e run. It
does not deploy, does not call DEV, does not run browser e2e, and does not read
secrets. The only executable gate in this milestone is:
secrets. The source contract gate is:
```sh
node scripts/m5-mvp-e2e-dry-run.mjs
@@ -13,6 +13,19 @@ The script reads the frozen protocol contracts, the DEV acceptance checklist,
validates the dependency graph and checks that each step has resolvable inputs
and outputs.
The L6 external acceptance CLI wraps the same report and dry-run plan:
```sh
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
This command defaults to a JSON dry-run plan. `--dry-run` is accepted for
backward compatibility, but it is no longer required for safety. The output
must identify the M0-M5 report source, artifact and DEV health contract counts,
two gateway sessions, two box resources, patch-panel status, agent and worker
session IDs, evidence records, open blockers, and the statement that no
DEV/PROD changes were made.
## Dry-Run Flow
| Order | Step | Purpose | Required Inputs | Required Outputs |
@@ -79,10 +92,14 @@ Do not switch to the real command until all of these are true:
8. No blocker class is open and a human has explicitly approved a real DEV
smoke.
Only after those gates are satisfied should the runner switch from dry-run to:
Only after those gates are satisfied should the runner switch from dry-run to
the explicit live DEV command:
```sh
hwlab-cli test e2e --env dev --mvp
hwlab-cli test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production
```
That command is intentionally not executed by M5.
That command is intentionally not executed by M5. While
`reports/dev-gate/dev-mvp-gate-report.json` contains open blockers, the CLI
must return `code: "BLOCKED"` with blocker evidence instead of substituting
fixture output as live DEV evidence.
+33
View File
@@ -89,6 +89,39 @@ reason.
| M4 agent automation loop | Source-only or dry-run, then MVP e2e | Agent manager, worker, skills, session lifecycle, scoped workspace, trace events, cleanup, and evidence chain are observable in fixtures or dry-run before any DEV runtime action. |
| M5 MVP e2e dry-run and acceptance | Dry-run, then MVP e2e | `npm run cli:dry-run` or equivalent names the MVP route, closed loops, evidence, and cleanup without mutation. Real acceptance is DEV-only, bounded, and requires prior DEV runtime smoke pass. |
## L6 CLI/Web Acceptance Entry
Use the L6 entry points for external manual acceptance triage. They read the
checked-in `reports/dev-gate/dev-mvp-gate-report.json` and
`fixtures/mvp/m5-e2e/dry-run-plan.json`; they do not call UniDesk runtime and
do not claim live DEV evidence from fixtures.
Safe source and dry-run checks:
```sh
npm run l6:smoke
npm run web:check
npm run cli:dry-run
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
The default CLI MVP command is a JSON dry-run plan even when `--dry-run` is not
provided. It must include the DEV endpoint, artifact/health counts, two
gateway sessions, two box resources, the patch-panel status, agent and worker
session IDs, evidence records, and a statement that no DEV/PROD changes were
made.
The live DEV entry point is intentionally separate:
```sh
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production
```
When the M0-M5 gate report still has open blockers, the live command must stop
with `code: "BLOCKED"` and include the blocker evidence. It must not use
fixture data, UniDesk services, PROD, heavy e2e, restarts, or secret reads to
produce a green result.
## Phase Procedures
### Source-Only Procedure
+295
View File
@@ -0,0 +1,295 @@
import fs from "node:fs";
import path from "node:path";
export const DEV_ENDPOINT = "http://74.48.78.17:6667";
export const DEFAULT_GATE_REPORT_PATH = "reports/dev-gate/dev-mvp-gate-report.json";
export const DEFAULT_M5_PLAN_PATH = "fixtures/mvp/m5-e2e/dry-run-plan.json";
function readJsonFile(repoRoot, relativePath) {
const absolutePath = path.resolve(repoRoot, relativePath);
const raw = fs.readFileSync(absolutePath, "utf8");
return JSON.parse(raw);
}
function countByServiceId(items) {
return new Map(items.map((item) => [item.serviceId, item]));
}
function requireArray(value, label) {
if (!Array.isArray(value)) {
throw new Error(`${label} must be an array`);
}
return value;
}
function assertRequiredMvpGate(report, plan) {
if (report.devOnly !== true || report.prodDisabled !== true) {
throw new Error("MVP gate report must be DEV-only and PROD-disabled");
}
if (report.gateStatus !== "blocked") {
throw new Error("MVP gate report must not claim live DEV pass in this MVP fixture");
}
if (plan.mode !== "dry-run" || plan.environment !== "dev" || plan.endpoint !== DEV_ENDPOINT) {
throw new Error("M5 plan must stay dry-run, dev, and pinned to the frozen DEV endpoint");
}
const milestones = requireArray(report.milestones, "report.milestones");
for (const milestone of ["M0", "M1", "M2", "M3", "M4", "M5"]) {
if (!milestones.some((item) => item.id === milestone)) {
throw new Error(`MVP gate report missing milestone ${milestone}`);
}
}
const artifacts = requireArray(plan.artifacts, "plan.artifacts");
const health = requireArray(plan.health, "plan.health");
const gatewaySessions = requireArray(plan.gatewaySessions, "plan.gatewaySessions");
const boxResources = requireArray(plan.boxResources, "plan.boxResources");
const evidenceRecords = requireArray(plan.evidenceRecords, "plan.evidenceRecords");
const operations = requireArray(plan.operations, "plan.operations");
const byService = countByServiceId(artifacts);
for (const serviceId of [
"hwlab-cloud-web",
"hwlab-cloud-api",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-cli"
]) {
if (!byService.has(serviceId)) {
throw new Error(`M5 plan missing artifact for ${serviceId}`);
}
}
if (gatewaySessions.length < 2) {
throw new Error("M5 plan must include two gateway sessions");
}
if (boxResources.length < 2) {
throw new Error("M5 plan must include two box resources");
}
if (!plan.patchPanelStatus?.patchPanelStatusId) {
throw new Error("M5 plan missing patch-panel status evidence");
}
if (!plan.agentSession?.agentSessionId || !plan.workerSession?.workerSessionId) {
throw new Error("M5 plan missing agent and worker session evidence");
}
if (evidenceRecords.length < 2 || operations.length < 2) {
throw new Error("M5 plan must include direct and agent evidence records");
}
}
function summarizeArtifact(artifact) {
return {
serviceId: artifact.serviceId,
commitId: artifact.commitId,
image: artifact.image,
tag: artifact.tag,
digest: artifact.digest,
digestNotApplicableReason: artifact.digestNotApplicableReason ?? null,
buildSource: artifact.buildSource,
deployEnv: artifact.deployEnv,
healthTimestamp: artifact.healthTimestamp
};
}
function summarizeHealth(health) {
return {
healthId: health.healthId,
serviceId: health.serviceId,
component: health.component,
status: health.status,
deployEnv: health.deployEnv,
endpoint: health.endpoint ?? null,
observedBy: health.observedBy,
dryRun: health.dryRun === true,
healthTimestamp: health.healthTimestamp
};
}
function summarizeMilestone(milestone) {
return {
id: milestone.id,
status: milestone.status,
commandCount: milestone.commands?.length ?? 0,
evidenceCount: milestone.evidence?.length ?? 0,
summary: milestone.summary
};
}
function summarizeBlocker(blocker) {
return {
type: blocker.type,
scope: blocker.scope,
status: blocker.status,
summary: blocker.summary
};
}
function summarizeStep(step) {
return {
id: step.id,
order: step.order,
title: step.title,
kind: step.kind,
requires: step.requires,
inputs: step.inputs,
outputs: step.outputs,
acceptanceStepIds: step.acceptanceStepIds
};
}
function summarizeGatewaySession(session) {
return {
gatewaySessionId: session.gatewaySessionId,
serviceId: session.serviceId,
gatewayId: session.gatewayId,
status: session.status,
environment: session.environment,
endpoint: session.endpoint ?? null
};
}
function summarizeBoxResource(resource) {
return {
resourceId: resource.resourceId,
gatewaySessionId: resource.gatewaySessionId,
boxId: resource.boxId,
resourceType: resource.resourceType,
name: resource.name,
state: resource.state,
environment: resource.environment
};
}
function summarizeEvidence(record) {
return {
evidenceId: record.evidenceId,
operationId: record.operationId,
agentSessionId: record.agentSessionId ?? null,
workerSessionId: record.workerSessionId ?? null,
kind: record.kind,
uri: record.uri,
sha256: record.sha256,
serviceId: record.serviceId,
environment: record.environment,
dryRun: record.metadata?.dryRun === true
};
}
export function buildMvpGateSummary({ report, plan }) {
assertRequiredMvpGate(report, plan);
const artifacts = plan.artifacts.map(summarizeArtifact);
const health = plan.health.map(summarizeHealth);
const milestones = report.milestones.map(summarizeMilestone);
const blockers = report.blockers.map(summarizeBlocker);
const steps = plan.steps.map(summarizeStep).sort((a, b) => a.order - b.order);
const blocked = blockers.some((blocker) => blocker.status === "open") || report.gateStatus === "blocked";
return {
generatedFrom: {
gateReport: DEFAULT_GATE_REPORT_PATH,
m5Plan: DEFAULT_M5_PLAN_PATH
},
issue: "pikasTech/HWLAB#56",
supports: ["pikasTech/HWLAB#7", "pikasTech/HWLAB#9"],
gateStatus: report.gateStatus,
blocked,
acceptanceLevel: report.acceptanceLevel,
reportCommitId: report.commitId,
dryRunOnly: true,
devOnly: report.devOnly === true,
prodDisabled: report.prodDisabled === true,
environment: plan.environment,
endpoint: plan.endpoint,
namespace: "hwlab-dev",
sourceSummary: report.sourceContract.summary,
localSmoke: report.localSmoke,
dryRun: report.dryRun,
devPreconditions: report.devPreconditions,
blockers,
milestones,
artifactCount: artifacts.length,
healthCount: health.length,
gatewaySessionCount: plan.gatewaySessions.length,
boxResourceCount: plan.boxResources.length,
patchPanelCount: 1,
operationCount: plan.operations.length,
evidenceCount: plan.evidenceRecords.length,
traceCount: plan.traceEvents.length,
auditCount: plan.auditEvents.length,
cleanupCount: plan.cleanup.length,
artifacts,
health,
topology: {
projectId: plan.project.projectId,
projectStatus: plan.project.status,
gateways: plan.gatewaySessions.map(summarizeGatewaySession),
boxResources: plan.boxResources.map(summarizeBoxResource),
patchPanel: {
patchPanelStatusId: plan.patchPanelStatus.patchPanelStatusId,
serviceId: plan.patchPanelStatus.serviceId,
state: plan.patchPanelStatus.state,
environment: plan.patchPanelStatus.environment,
activeConnectionCount: plan.patchPanelStatus.activeConnections.length,
activeConnections: plan.patchPanelStatus.activeConnections
}
},
agent: {
agentSessionId: plan.agentSession.agentSessionId,
agentServiceId: plan.agentSession.serviceId,
agentStatus: plan.agentSession.status,
workerSessionId: plan.workerSession.workerSessionId,
workerServiceId: plan.workerSession.serviceId,
workerStatus: plan.workerSession.status,
projectId: plan.agentSession.projectId,
environment: plan.agentSession.environment,
cleanupId: plan.agentSession.metadata.cleanupId
},
operations: plan.operations.map((operation) => ({
operationId: operation.operationId,
requestedBy: operation.requestedBy,
status: operation.status,
resourceId: operation.resourceId,
capabilityId: operation.capabilityId,
agentSessionId: operation.agentSessionId ?? null,
workerSessionId: operation.workerSessionId ?? null,
environment: operation.environment
})),
evidenceRecords: plan.evidenceRecords.map(summarizeEvidence),
steps,
realDevGate: {
sourcePlanCommand: plan.realDevGate.command,
command: `${plan.realDevGate.command} --live --confirm-dev --confirmed-non-production`,
requiresAllBlockersClear: plan.realDevGate.requiresAllBlockersClear,
requiresDryRunPass: plan.realDevGate.requiresDryRunPass,
requiresArtifactObservability: plan.realDevGate.requiresArtifactObservability,
requiresD601RouteObservation: plan.realDevGate.requiresD601RouteObservation,
requiresHumanApprovalForRealDev: plan.realDevGate.requiresHumanApprovalForRealDev
},
safety: {
mode: plan.mode,
allowNetwork: plan.safety.allowNetwork,
allowDeploy: plan.safety.allowDeploy,
allowBrowserE2E: plan.safety.allowBrowserE2E,
allowSecrets: plan.safety.allowSecrets,
prohibitedActions: plan.safety.prohibitedActions
}
};
}
export function loadMvpGateSummary(repoRoot, options = {}) {
const reportPath = options.reportPath ?? DEFAULT_GATE_REPORT_PATH;
const planPath = options.planPath ?? DEFAULT_M5_PLAN_PATH;
const report = readJsonFile(repoRoot, reportPath);
const plan = readJsonFile(repoRoot, planPath);
const summary = buildMvpGateSummary({ report, plan });
summary.generatedFrom = {
gateReport: reportPath,
m5Plan: planPath
};
return summary;
}
+4 -2
View File
@@ -5,15 +5,17 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
"dev:evidence:check": "node scripts/dev-evidence-blocker-aggregator.mjs --check",
"dev-edge:smoke": "node scripts/dev-edge-health-smoke.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:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
"l6:smoke": "node scripts/l6-cli-web-smoke.mjs",
"web:summary": "node scripts/export-web-gate-summary.mjs",
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs",
"artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const outputPath = path.join(repoRoot, "web/hwlab-cloud-web/gate-summary.mjs");
const summary = loadMvpGateSummary(repoRoot);
const body = `// Generated from reports/dev-gate/dev-mvp-gate-report.json and fixtures/mvp/m5-e2e/dry-run-plan.json.
// Run node scripts/export-web-gate-summary.mjs after changing those fixtures.
export const gateSummary = ${JSON.stringify(summary, null, 2)};
`;
fs.writeFileSync(outputPath, body);
process.stdout.write(`wrote ${path.relative(repoRoot, outputPath)}\n`);
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
import { gateSummary } from "../web/hwlab-cloud-web/gate-summary.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
async function captureCli(args) {
let stdout = "";
let stderr = "";
const exitCode = await runCli(args, {
cwd: repoRoot,
stdout: {
write(chunk) {
stdout += chunk;
}
},
stderr: {
write(chunk) {
stderr += chunk;
}
}
});
return { exitCode, stdout, stderr };
}
const summary = loadMvpGateSummary(repoRoot);
assert.equal(summary.endpoint, "http://74.48.78.17:6667");
assert.deepEqual(
summary.milestones.map((item) => item.id),
["M0", "M1", "M2", "M3", "M4", "M5"]
);
const dryRun = await captureCli(["test", "e2e", "--env", "dev", "--mvp"]);
assert.equal(dryRun.exitCode, 0, dryRun.stderr);
const dryRunJson = JSON.parse(dryRun.stdout);
assert.equal(dryRunJson.mode, "dry-run");
assert.equal(dryRunJson.environment, "dev");
assert.equal(dryRunJson.endpoint, summary.endpoint);
assert.equal(dryRunJson.contractStatus.artifacts.count, 13);
assert.equal(dryRunJson.contractStatus.health.count, 14);
assert.equal(dryRunJson.contractStatus.topology.gatewaySessions, 2);
assert.equal(dryRunJson.contractStatus.topology.boxResources, 2);
assert.equal(dryRunJson.contractStatus.topology.patchPanelStatusId, "pps_m5-0001");
assert.equal(dryRunJson.contractStatus.agent.agentSessionId, "agt_m5-0001");
assert.equal(dryRunJson.contractStatus.evidence.records.length, 2);
assert.match(dryRunJson.statement, /no DEV\/PROD changes/);
const explicitDryRun = await captureCli(["test", "e2e", "--env", "dev", "--mvp", "--dry-run"]);
assert.equal(explicitDryRun.exitCode, 0, explicitDryRun.stderr);
assert.equal(JSON.parse(explicitDryRun.stdout).mode, "dry-run");
const liveBlocked = await captureCli([
"test",
"e2e",
"--env",
"dev",
"--mvp",
"--live",
"--confirm-dev",
"--confirmed-non-production"
]);
assert.equal(liveBlocked.exitCode, 2);
const liveBlockedJson = JSON.parse(liveBlocked.stderr);
assert.equal(liveBlockedJson.code, "BLOCKED");
assert.equal(liveBlockedJson.blockers[0].type, "network_blocker");
assert.equal(gateSummary.endpoint, summary.endpoint);
assert.equal(gateSummary.gateStatus, "blocked");
assert.equal(gateSummary.artifactCount, 13);
assert.equal(gateSummary.healthCount, 14);
assert.equal(gateSummary.gatewaySessionCount, 2);
assert.equal(gateSummary.boxResourceCount, 2);
assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.evidenceRecords.length, 2);
console.log("L6 CLI/Web smoke passed: CLI and Web read the M0-M5 gate report");
+13 -5
View File
@@ -310,11 +310,19 @@ async function smokeCliDryRunFixture() {
});
assert.equal(exitCode, 0, stderr);
assert.match(stdout, /hwlab-cli test e2e --env dev --mvp --dry-run/);
assert.match(stdout, /cloud-web\/cloud-api/);
assert.match(stdout, /hardware trusted closed loop/);
assert.match(stdout, /evidence_mvp-runtime-plan/);
assert.match(stdout, /dry-run only: no DEV\/PROD changes were made/);
const output = JSON.parse(stdout);
assert.equal(output.command, "hwlab-cli test e2e --env dev --mvp --dry-run");
assert.equal(output.mode, "dry-run");
assert.equal(output.environment, "dev");
assert.equal(output.endpoint, DEV_ENDPOINT);
assert.equal(output.safety.allowNetwork, false);
assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-web"), true);
assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-api"), true);
assert.equal(output.contractStatus.topology.gatewaySessions, 2);
assert.equal(output.contractStatus.topology.boxResources, 2);
assert.equal(output.contractStatus.agent.agentSessionId, "agt_m5-0001");
assert.equal(output.contractStatus.evidence.records.length, 2);
assert.match(output.statement, /no DEV\/PROD changes/);
logOk("CLI dry-run fixture parse");
}
+199 -122
View File
@@ -1,60 +1,4 @@
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: []
};
}
import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
function formatJson(value) {
return JSON.stringify(value, null, 2);
@@ -64,60 +8,8 @@ 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 writeJson(stream, value) {
writeLine(stream, formatJson(value));
}
function parseArgs(args) {
@@ -127,11 +19,12 @@ function parseArgs(args) {
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 {
flags.add(arg);
}
} else {
rest.push(arg);
@@ -140,46 +33,230 @@ function parseArgs(args) {
return { flags, options, rest };
}
function makeError(code, message, details = {}) {
return {
ok: false,
code,
message,
...details
};
}
function mvpGateOverview(summary) {
return {
ok: true,
command: "hwlab-cli health",
environment: summary.environment,
endpoint: summary.endpoint,
gateStatus: summary.gateStatus,
devOnly: summary.devOnly,
prodDisabled: summary.prodDisabled,
reportCommitId: summary.reportCommitId,
artifactCount: summary.artifactCount,
healthCount: summary.healthCount,
milestoneStatus: Object.fromEntries(summary.milestones.map((item) => [item.id, item.status])),
blocked: summary.blocked,
blockers: summary.blockers
};
}
function mvpProjectList(summary) {
return {
ok: true,
command: "hwlab-cli project list",
environment: summary.environment,
endpoint: summary.endpoint,
projects: [
{
projectId: summary.topology.projectId,
name: "HWLAB M5 MVP E2E Dry Run",
status: summary.topology.projectStatus,
environment: summary.environment,
source: summary.generatedFrom.m5Plan
}
],
topology: {
gatewaySessions: summary.topology.gateways.map((gateway) => gateway.gatewaySessionId),
boxResources: summary.topology.boxResources.map((resource) => resource.resourceId),
patchPanelStatusId: summary.topology.patchPanel.patchPanelStatusId
}
};
}
function mvpDryRunPlan(summary, command) {
return {
ok: true,
command,
mode: "dry-run",
dryRunOnly: true,
environment: summary.environment,
endpoint: summary.endpoint,
gateStatus: summary.gateStatus,
blocked: summary.blocked,
generatedFrom: summary.generatedFrom,
safety: summary.safety,
milestones: summary.milestones,
contractStatus: {
artifacts: {
status: "fixture",
count: summary.artifactCount,
commitIds: [...new Set(summary.artifacts.map((artifact) => artifact.commitId))],
services: summary.artifacts.map((artifact) => artifact.serviceId)
},
health: {
status: "fixture",
count: summary.healthCount,
components: summary.health.map((health) => ({
component: health.component,
serviceId: health.serviceId,
status: health.status,
dryRun: health.dryRun
}))
},
topology: {
projectId: summary.topology.projectId,
gatewaySessions: summary.gatewaySessionCount,
boxResources: summary.boxResourceCount,
patchPanelStatusId: summary.topology.patchPanel.patchPanelStatusId,
activeConnectionCount: summary.topology.patchPanel.activeConnectionCount
},
agent: summary.agent,
evidence: {
operations: summary.operationCount,
traceEvents: summary.traceCount,
auditEvents: summary.auditCount,
records: summary.evidenceRecords,
cleanupRecords: summary.cleanupCount
}
},
steps: summary.steps.map((step) => ({
id: step.id,
order: step.order,
title: step.title,
kind: step.kind,
requires: step.requires
})),
blockers: summary.blockers,
realDevGate: summary.realDevGate,
statement: "dry-run only: no DEV/PROD changes were made"
};
}
function mvpLiveBlocked(summary, command) {
return {
ok: false,
code: "BLOCKED",
command,
mode: "live-dev",
environment: summary.environment,
endpoint: summary.endpoint,
gateStatus: summary.gateStatus,
blocked: true,
blockers: summary.blockers,
requiredBeforeRetry: [
"Clear all open blockers in reports/dev-gate/dev-mvp-gate-report.json.",
"Record live HWLAB DEV health for /health and /live on the frozen DEV endpoint.",
"Keep the run DEV-only; do not use UniDesk runtime or PROD substitutes."
],
generatedFrom: summary.generatedFrom
};
}
function writeHelp(stdout) {
writeJson(stdout, {
ok: true,
command: "hwlab-cli",
commands: [
"health",
"project list",
"test e2e --env dev --mvp",
"test e2e --env dev --mvp --dry-run",
"test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production"
],
defaults: {
testE2eMvp: "dry-run plan",
output: "json",
devEndpoint: DEV_ENDPOINT
}
});
}
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 repoRoot = io.cwd ?? process.cwd();
let summary;
try {
summary = loadMvpGateSummary(repoRoot);
} catch (error) {
writeJson(stderr, makeError("GATE_SUMMARY_UNREADABLE", error.message));
return 1;
}
const [command = "help", subcommand, ...rest] = argv;
const { flags, options } = parseArgs(rest);
if (command === "health") {
writeHealth(runtime, stdout);
writeJson(stdout, mvpGateOverview(summary));
return 0;
}
if (command === "project" && subcommand === "list") {
writeProjectList(runtime, stdout);
writeJson(stdout, mvpProjectList(summary));
return 0;
}
if (command === "test" && subcommand === "e2e") {
const env = options.get("--env") ?? null;
const isDryRun = flags.has("--dry-run");
const isMvp = flags.has("--mvp");
const isLive = flags.has("--live");
const isDryRun = flags.has("--dry-run") || !isLive;
const commandLine = `hwlab-cli test e2e --env ${env ?? "<missing>"}${isMvp ? " --mvp" : ""}${
isLive ? " --live" : isDryRun && flags.has("--dry-run") ? " --dry-run" : ""
}`;
if (env !== "dev") {
writeLine(stderr, "only --env dev is supported in the skeleton");
writeJson(stderr, makeError("UNSUPPORTED_ENV", "Only --env dev is supported for the MVP gate.", { env }));
return 1;
}
if (!isMvp || !isDryRun) {
writeLine(stderr, "use --mvp --dry-run for the skeleton");
if (!isMvp) {
writeJson(stderr, makeError("MISSING_MVP_FLAG", "Use --mvp for the HWLAB MVP E2E gate."));
return 1;
}
writeDryRun(runtime, stdout);
if (isLive) {
if (!flags.has("--confirm-dev") || !flags.has("--confirmed-non-production")) {
writeJson(
stderr,
makeError("LIVE_CONFIRMATION_REQUIRED", "Live DEV requires --confirm-dev --confirmed-non-production.", {
requiredFlags: ["--confirm-dev", "--confirmed-non-production"]
})
);
return 1;
}
if (summary.blocked) {
writeJson(stderr, mvpLiveBlocked(summary, commandLine));
return 2;
}
writeJson(
stderr,
makeError("LIVE_DEV_NOT_IMPLEMENTED", "The MVP CLI does not execute live DEV e2e from this runner.")
);
return 2;
}
writeJson(stdout, mvpDryRunPlan(summary, commandLine));
return 0;
}
if (command === "project" && !subcommand) {
writeLine(stderr, "usage: hwlab-cli project list");
writeJson(stderr, makeError("USAGE", "usage: hwlab-cli project list"));
return 1;
}
if (command === "test" && !subcommand) {
writeLine(stderr, "usage: hwlab-cli test e2e --env dev --mvp --dry-run");
writeJson(stderr, makeError("USAGE", "usage: hwlab-cli test e2e --env dev --mvp"));
return 1;
}
+1 -1
View File
@@ -7,7 +7,7 @@
"hwlab-cli": "./bin/hwlab-cli.mjs"
},
"scripts": {
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs",
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node ../../scripts/l6-cli-web-smoke.mjs",
"health": "node bin/hwlab-cli.mjs health"
}
}
+139 -31
View File
@@ -1,40 +1,148 @@
import { runtime } from "./runtime.mjs";
import { gateSummary } from "./gate-summary.mjs";
const runtimeGrid = document.querySelector("#runtime-grid");
const steps = document.querySelector("#mvp-steps");
const projectsBody = document.querySelector("#projects-body");
const status = document.querySelector("#status");
const metaGrid = document.querySelector("#meta-grid");
const milestoneGrid = document.querySelector("#milestone-grid");
const healthBody = document.querySelector("#health-body");
const artifactBody = document.querySelector("#artifact-body");
const topologyGrid = document.querySelector("#topology-grid");
const agentGrid = document.querySelector("#agent-grid");
const evidenceBody = document.querySelector("#evidence-body");
const blockerList = document.querySelector("#blocker-list");
const commandList = document.querySelector("#command-list");
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);
function appendText(parent, tagName, text, className) {
const element = document.createElement(tagName);
element.textContent = text;
if (className) {
element.className = className;
}
parent.append(element);
return element;
}
for (const step of runtime.mvpSteps) {
function appendMetricGrid(parent, pairs) {
for (const [label, value] of pairs) {
const item = document.createElement("div");
item.className = "metric";
appendText(item, "span", label, "metric-label");
appendText(item, "strong", String(value ?? "n/a"), "metric-value");
parent.append(item);
}
}
function badge(text, tone = text) {
const span = document.createElement("span");
span.className = `badge tone-${String(tone).replace(/[^a-z0-9_-]/gi, "-").toLowerCase()}`;
span.textContent = text;
return span;
}
function cell(text, className) {
const td = document.createElement("td");
td.textContent = text ?? "n/a";
if (className) {
td.className = className;
}
return td;
}
status.replaceChildren(badge(gateSummary.gateStatus, gateSummary.gateStatus));
appendMetricGrid(metaGrid, [
["Environment", gateSummary.environment],
["Endpoint", gateSummary.endpoint],
["Namespace", gateSummary.namespace],
["Report commit", gateSummary.reportCommitId],
["Gate report", gateSummary.generatedFrom.gateReport],
["M5 plan", gateSummary.generatedFrom.m5Plan]
]);
for (const milestone of gateSummary.milestones) {
const item = document.createElement("article");
item.className = "milestone";
const head = document.createElement("div");
head.className = "milestone-head";
appendText(head, "strong", milestone.id);
head.append(badge(milestone.status, milestone.status));
item.append(head);
appendText(item, "p", milestone.summary);
appendText(item, "small", `${milestone.commandCount} commands / ${milestone.evidenceCount} evidence lines`);
milestoneGrid.append(item);
}
for (const row of gateSummary.health) {
const tr = document.createElement("tr");
tr.append(cell(row.component));
tr.append(cell(row.serviceId, "mono"));
const statusCell = document.createElement("td");
statusCell.append(badge(row.status, row.status));
tr.append(statusCell);
tr.append(cell(row.observedBy));
tr.append(cell(row.endpoint ?? gateSummary.endpoint, "mono wrap"));
healthBody.append(tr);
}
for (const artifact of gateSummary.artifacts) {
const tr = document.createElement("tr");
tr.append(cell(artifact.serviceId, "mono"));
tr.append(cell(artifact.commitId, "mono"));
tr.append(cell(`${artifact.image}:${artifact.tag}`, "mono wrap"));
tr.append(cell(artifact.digest, "mono wrap"));
tr.append(cell(artifact.healthTimestamp, "mono"));
artifactBody.append(tr);
}
appendMetricGrid(topologyGrid, [
["Project", gateSummary.topology.projectId],
["Gateways", gateSummary.gatewaySessionCount],
["Box resources", gateSummary.boxResourceCount],
["Patch panel", gateSummary.topology.patchPanel.patchPanelStatusId],
["Active links", gateSummary.topology.patchPanel.activeConnectionCount],
[
"Connection",
gateSummary.topology.patchPanel.activeConnections
.map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`)
.join(", ")
]
]);
appendMetricGrid(agentGrid, [
["Agent session", gateSummary.agent.agentSessionId],
["Agent service", gateSummary.agent.agentServiceId],
["Worker session", gateSummary.agent.workerSessionId],
["Worker service", gateSummary.agent.workerServiceId],
["Trace events", gateSummary.traceCount],
["Audit events", gateSummary.auditCount],
["Cleanup records", gateSummary.cleanupCount],
["Cleanup id", gateSummary.agent.cleanupId]
]);
for (const record of gateSummary.evidenceRecords) {
const tr = document.createElement("tr");
tr.append(cell(record.evidenceId, "mono"));
tr.append(cell(record.operationId, "mono"));
tr.append(cell(record.serviceId, "mono"));
tr.append(cell(record.uri, "mono wrap"));
tr.append(cell(record.sha256, "mono wrap"));
evidenceBody.append(tr);
}
for (const blocker of gateSummary.blockers) {
const li = document.createElement("li");
li.textContent = step;
steps.append(li);
const head = document.createElement("div");
head.className = "blocker-head";
head.append(badge(blocker.type, "blocked"));
appendText(head, "strong", blocker.scope);
li.append(head);
appendText(li, "p", blocker.summary);
blockerList.append(li);
}
for (const project of runtime.projects) {
const row = document.createElement("tr");
row.innerHTML = `
<td>${project.projectId}<br /><strong>${project.name}</strong></td>
<td>${project.status}</td>
<td>${project.environment}</td>
<td>${project.updatedAt}</td>
`;
projectsBody.append(row);
for (const command of [
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production",
"node scripts/m5-mvp-e2e-dry-run.mjs"
]) {
appendText(commandList, "li", command, "mono");
}
+809
View File
@@ -0,0 +1,809 @@
// Generated from reports/dev-gate/dev-mvp-gate-report.json and fixtures/mvp/m5-e2e/dry-run-plan.json.
// Run node scripts/export-web-gate-summary.mjs after changing those fixtures.
export const gateSummary = {
"generatedFrom": {
"gateReport": "reports/dev-gate/dev-mvp-gate-report.json",
"m5Plan": "fixtures/mvp/m5-e2e/dry-run-plan.json"
},
"issue": "pikasTech/HWLAB#56",
"supports": [
"pikasTech/HWLAB#7",
"pikasTech/HWLAB#9"
],
"gateStatus": "blocked",
"blocked": true,
"acceptanceLevel": "dev_mvp_gate",
"reportCommitId": "6eb4c199b0c63cca7fd68fc3306f7a9cbe4323ea",
"dryRunOnly": true,
"devOnly": true,
"prodDisabled": true,
"environment": "dev",
"endpoint": "http://74.48.78.17:6667",
"namespace": "hwlab-dev",
"sourceSummary": "The DEV gate is anchored to the frozen matrix, gate contract, and M0-M5 milestone docs on origin/main.",
"localSmoke": {
"status": "pass",
"commands": [
"node scripts/validate-contract.mjs",
"node scripts/validate-m0-contract.mjs",
"node scripts/validate-artifact-catalog.mjs",
"node scripts/validate-runtime-boundary.mjs",
"node scripts/validate-schema-drift-map.mjs",
"node scripts/validate-evidence-chain.mjs",
"node scripts/m1-contract-smoke.mjs",
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
"node scripts/m3-hardware-loop-smoke.mjs",
"node scripts/m4-agent-loop-smoke.mjs"
],
"evidence": [
"validated 15 JSON files and 13 service ids",
"validated M0 contract examples: 7 JSON files, 13 service ids",
"validated 13 DEV artifact catalog services at 24eb3bf",
"validated HWLAB runtime boundary guard for 13 services; forbidden substitutes: unidesk-backend, provider-gateway, microservice-proxy",
"validated schema drift map with 59 aligned fields and 12 gaps",
"validated 2 evidence chain fixtures and 2 evidence records",
"[m1-smoke] passed",
"[m2-smoke] validated 11 service contracts",
"[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:6667",
"[m2-smoke] no real DEV/PROD request was made",
"M3 hardware loop smoke passed",
"[m4-smoke] ok m4-agent-automation-loop-local-contract-smoke"
],
"summary": "M0-M4 contract and smoke checks pass locally without contacting the live DEV ingress."
},
"dryRun": {
"status": "pass",
"commands": [
"node tools/hwlab-cli/bin/hwlab-cli.mjs health",
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
"node scripts/m5-mvp-e2e-dry-run.mjs"
],
"evidence": [
"hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2",
"hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls",
"M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls"
],
"summary": "The M5 orchestration dry-run is green, but it remains fixture-only and does not establish a live DEV gate."
},
"devPreconditions": {
"status": "blocked",
"requirements": [
"Obtain a live HTTP 200/JSON health observation from http://74.48.78.17:6667/health and /live",
"Record live route observations for hwlab-edge-proxy, hwlab-tunnel-client, hwlab-router, hwlab-cloud-api/web, hwlab-gateway, hwlab-gateway-simu, hwlab-box-simu, hwlab-patch-panel, hwlab-agent-mgr, hwlab-agent-worker, and hwlab-agent-skills",
"Keep the run DEV-only and avoid PROD, heavy e2e, secrets, restarts, or UniDesk substitution"
],
"commands": [
"curl -fsS --max-time 5 http://74.48.78.17:6667/health",
"curl -fsS --max-time 5 http://74.48.78.17:6667/live"
],
"evidence": [
"curl exit 7 for /health: could not connect to 74.48.78.17 port 6667",
"curl exit 7 for /live: could not connect to 74.48.78.17 port 6667"
],
"summary": "The frozen DEV endpoint is not reachable from this runner, so no live DEV gate can be claimed yet."
},
"blockers": [
{
"type": "network_blocker",
"scope": "devPreconditions",
"status": "open",
"summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing."
}
],
"milestones": [
{
"id": "M0",
"status": "pass",
"commandCount": 6,
"evidenceCount": 6,
"summary": "Frozen contract, artifact, boundary, drift, and evidence fixtures are parseable and aligned to the DEV boundary."
},
{
"id": "M1",
"status": "pass",
"commandCount": 1,
"evidenceCount": 4,
"summary": "The local M1 contract smoke passes."
},
{
"id": "M2",
"status": "pass",
"commandCount": 1,
"evidenceCount": 3,
"summary": "The deploy smoke remains fixture-only and validates the frozen route phases and artifact metadata."
},
{
"id": "M3",
"status": "pass",
"commandCount": 1,
"evidenceCount": 3,
"summary": "The local hardware trusted loop smoke passes."
},
{
"id": "M4",
"status": "pass",
"commandCount": 1,
"evidenceCount": 1,
"summary": "The local agent automation loop smoke passes."
},
{
"id": "M5",
"status": "pass",
"commandCount": 3,
"evidenceCount": 3,
"summary": "The M5 orchestration is green in dry-run mode, but it stays fixture-only and does not claim live DEV readiness."
}
],
"artifactCount": 13,
"healthCount": 14,
"gatewaySessionCount": 2,
"boxResourceCount": 2,
"patchPanelCount": 1,
"operationCount": 2,
"evidenceCount": 2,
"traceCount": 8,
"auditCount": 7,
"cleanupCount": 2,
"artifacts": [
{
"serviceId": "hwlab-edge-proxy",
"commitId": "caa9ed0",
"image": "hwlab-edge-proxy",
"tag": "dry-run-caa9ed0",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-tunnel-client",
"commitId": "caa9ed0",
"image": "hwlab-tunnel-client",
"tag": "dry-run-caa9ed0",
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-router",
"commitId": "caa9ed0",
"image": "hwlab-router",
"tag": "dry-run-caa9ed0",
"digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-cloud-api",
"commitId": "caa9ed0",
"image": "hwlab-cloud-api",
"tag": "dry-run-caa9ed0",
"digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-cloud-web",
"commitId": "caa9ed0",
"image": "hwlab-cloud-web-static-assets",
"tag": "dry-run-caa9ed0",
"digest": "not_applicable",
"digestNotApplicableReason": "static asset digest is produced by the future DEV build pipeline, not this local dry-run fixture",
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-gateway",
"commitId": "caa9ed0",
"image": "hwlab-gateway",
"tag": "dry-run-caa9ed0",
"digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-gateway-simu",
"commitId": "caa9ed0",
"image": "hwlab-gateway-simu",
"tag": "dry-run-caa9ed0",
"digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-box-simu",
"commitId": "caa9ed0",
"image": "hwlab-box-simu",
"tag": "dry-run-caa9ed0",
"digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-patch-panel",
"commitId": "caa9ed0",
"image": "hwlab-patch-panel",
"tag": "dry-run-caa9ed0",
"digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-agent-mgr",
"commitId": "caa9ed0",
"image": "hwlab-agent-mgr",
"tag": "dry-run-caa9ed0",
"digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-agent-worker",
"commitId": "caa9ed0",
"image": "hwlab-agent-worker",
"tag": "dry-run-caa9ed0",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"digestNotApplicableReason": null,
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-agent-skills",
"commitId": "caa9ed0",
"image": "hwlab-agent-runtime-skills",
"tag": "dry-run-caa9ed0",
"digest": "not_applicable",
"digestNotApplicableReason": "skill bundle digest is produced by the future DEV build pipeline, not this local dry-run fixture",
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"serviceId": "hwlab-cli",
"commitId": "caa9ed0",
"image": "hwlab-cli-npm-package",
"tag": "dry-run-caa9ed0",
"digest": "not_applicable",
"digestNotApplicableReason": "local CLI package is not containerized in this dry-run fixture",
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
"deployEnv": "dev",
"healthTimestamp": "2026-05-21T00:00:00.000Z"
}
],
"health": [
{
"healthId": "hlt_m5-dev-ingress",
"serviceId": "hwlab-edge-proxy",
"component": "DEV ingress",
"status": "healthy",
"deployEnv": "dev",
"endpoint": "http://74.48.78.17:6667/health",
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-edge-route",
"serviceId": "hwlab-edge-proxy",
"component": "master edge proxy",
"status": "healthy",
"deployEnv": "dev",
"endpoint": "http://74.48.78.17:6667",
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-frp",
"serviceId": "hwlab-tunnel-client",
"component": "frp",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-router",
"serviceId": "hwlab-router",
"component": "D601 router",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-cloud-api",
"serviceId": "hwlab-cloud-api",
"component": "Cloud API",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-cloud-web",
"serviceId": "hwlab-cloud-web",
"component": "Cloud Web",
"status": "healthy",
"deployEnv": "dev",
"endpoint": "http://74.48.78.17:6667",
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-gateway",
"serviceId": "hwlab-gateway",
"component": "Gateway",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-gateway-simu",
"serviceId": "hwlab-gateway-simu",
"component": "Gateway simulator",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-box-simu",
"serviceId": "hwlab-box-simu",
"component": "Box simulator",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-patch-panel",
"serviceId": "hwlab-patch-panel",
"component": "Patch panel",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-agent-mgr",
"serviceId": "hwlab-agent-mgr",
"component": "Agent manager",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-agent-worker",
"serviceId": "hwlab-agent-worker",
"component": "Agent worker",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-agent-skills",
"serviceId": "hwlab-agent-skills",
"component": "Agent skills",
"status": "healthy",
"deployEnv": "dev",
"endpoint": null,
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
},
{
"healthId": "hlt_m5-cli",
"serviceId": "hwlab-cli",
"component": "CLI",
"status": "healthy",
"deployEnv": "dev",
"endpoint": "http://74.48.78.17:6667",
"observedBy": "fixture",
"dryRun": true,
"healthTimestamp": "2026-05-21T00:00:00.000Z"
}
],
"topology": {
"projectId": "proj_m5-mvp-e2e",
"projectStatus": "active",
"gateways": [
{
"gatewaySessionId": "gws_m5-0001",
"serviceId": "hwlab-gateway",
"gatewayId": "gtw_m5-dev-boundary",
"status": "connected",
"environment": "dev",
"endpoint": "http://74.48.78.17:6667/gateway"
},
{
"gatewaySessionId": "gws_m5-simu-0001",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gtw_m5-simu",
"status": "connected",
"environment": "dev",
"endpoint": null
}
],
"boxResources": [
{
"resourceId": "res_m5-control-relay",
"gatewaySessionId": "gws_m5-0001",
"boxId": "box_m5-control",
"resourceType": "relay",
"name": "M5 control relay",
"state": "available",
"environment": "dev"
},
{
"resourceId": "res_m5-target-board",
"gatewaySessionId": "gws_m5-0001",
"boxId": "box_m5-target",
"resourceType": "board",
"name": "M5 target board",
"state": "available",
"environment": "dev"
}
],
"patchPanel": {
"patchPanelStatusId": "pps_m5-0001",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnectionCount": 1,
"activeConnections": [
{
"fromResourceId": "res_m5-control-relay",
"fromPort": "out1",
"toResourceId": "res_m5-target-board",
"toPort": "reset"
}
]
}
},
"agent": {
"agentSessionId": "agt_m5-0001",
"agentServiceId": "hwlab-agent-mgr",
"agentStatus": "completed",
"workerSessionId": "wkr_m5-0001",
"workerServiceId": "hwlab-agent-worker",
"workerStatus": "completed",
"projectId": "proj_m5-mvp-e2e",
"environment": "dev",
"cleanupId": "cln_m5-worker"
},
"operations": [
{
"operationId": "op_m5-direct-0001",
"requestedBy": "user_m5-dry-run",
"status": "succeeded",
"resourceId": "res_m5-control-relay",
"capabilityId": "cap_m5-relay-set",
"agentSessionId": null,
"workerSessionId": null,
"environment": "dev"
},
{
"operationId": "op_m5-agent-0001",
"requestedBy": "agent_m5-dry-run",
"status": "succeeded",
"resourceId": "res_m5-control-relay",
"capabilityId": "cap_m5-relay-set",
"agentSessionId": "agt_m5-0001",
"workerSessionId": "wkr_m5-0001",
"environment": "dev"
}
],
"evidenceRecords": [
{
"evidenceId": "evi_m5-direct-0001",
"operationId": "op_m5-direct-0001",
"agentSessionId": null,
"workerSessionId": null,
"kind": "trace",
"uri": "fixtures/mvp/m5-e2e/evidence/direct-operation.txt",
"sha256": "4cd5cc2c942501b55f7a32f4184e9a1c920b31f13110f44ec68ddc0ad7dc1e34",
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"dryRun": true
},
{
"evidenceId": "evi_m5-agent-0001",
"operationId": "op_m5-agent-0001",
"agentSessionId": "agt_m5-0001",
"workerSessionId": "wkr_m5-0001",
"kind": "trace",
"uri": "fixtures/mvp/m5-e2e/evidence/agent-operation.txt",
"sha256": "96e7ffa99efd78b3d5d1b1dfd08be2f077f618dacf786b47f764794714c15c1c",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"dryRun": true
}
],
"steps": [
{
"id": "step-dev-health",
"order": 1,
"title": "DEV health",
"kind": "health",
"requires": [],
"inputs": [
"endpoint",
"health:hlt_m5-dev-ingress",
"health:hlt_m5-edge-route",
"health:hlt_m5-frp",
"health:hlt_m5-router",
"health:hlt_m5-cloud-api",
"health:hlt_m5-cloud-web"
],
"outputs": [
"route_m5-dev",
"tun_m5-d601",
"hwlab-dev",
"hwlab-cloud-api",
"hwlab-cloud-web"
],
"acceptanceStepIds": [
"static-contract-parse",
"endpoint-freeze",
"dev-ingress-health",
"edge-route",
"frp-tunnel",
"d601-router",
"cloud-surface"
]
},
{
"id": "step-gateway-box-status",
"order": 2,
"title": "gateway/box status",
"kind": "status",
"requires": [
"step-dev-health"
],
"inputs": [
"gatewaySession:gws_m5-0001",
"gatewaySession:gws_m5-simu-0001",
"boxResource:res_m5-control-relay",
"boxResource:res_m5-target-board",
"capability:cap_m5-relay-set",
"capability:cap_m5-board-reset"
],
"outputs": [
"gws_m5-0001",
"res_m5-control-relay",
"res_m5-target-board",
"cap_m5-relay-set",
"cap_m5-board-reset"
],
"acceptanceStepIds": [
"gateway-sim-patch-panel"
]
},
{
"id": "step-wiring",
"order": 3,
"title": "wiring",
"kind": "wiring",
"requires": [
"step-gateway-box-status"
],
"inputs": [
"wiringConfig:wir_m5-0001",
"patchPanelStatus:pps_m5-0001"
],
"outputs": [
"wir_m5-0001",
"pps_m5-0001"
],
"acceptanceStepIds": [
"gateway-sim-patch-panel"
]
},
{
"id": "step-direct-hardware-call",
"order": 4,
"title": "direct hardware call",
"kind": "hardware_operation",
"requires": [
"step-wiring"
],
"inputs": [
"rpc:direct-hardware-request",
"operation:op_m5-direct-0001",
"resource:res_m5-control-relay",
"capability:cap_m5-relay-set",
"patchPanelStatus:pps_m5-0001"
],
"outputs": [
"op_m5-direct-0001",
"trc_m5-direct-0001"
],
"acceptanceStepIds": [
"gateway-sim-patch-panel"
]
},
{
"id": "step-audit",
"order": 5,
"title": "audit",
"kind": "audit",
"requires": [
"step-direct-hardware-call"
],
"inputs": [
"audit:aud_m5-project-selected",
"audit:aud_m5-gateway-started",
"audit:aud_m5-wiring-applied",
"audit:aud_m5-direct-operation"
],
"outputs": [
"aud_m5-direct-operation"
],
"acceptanceStepIds": [
"artifact-observability"
]
},
{
"id": "step-agent-session",
"order": 6,
"title": "agent session",
"kind": "agent",
"requires": [
"step-audit"
],
"inputs": [
"agentSession:agt_m5-0001",
"workerSession:wkr_m5-0001",
"health:hlt_m5-agent-mgr",
"health:hlt_m5-agent-worker",
"health:hlt_m5-agent-skills"
],
"outputs": [
"agt_m5-0001",
"wkr_m5-0001"
],
"acceptanceStepIds": [
"agent-runtime"
]
},
{
"id": "step-agent-hardware-call",
"order": 7,
"title": "agent hardware call",
"kind": "hardware_operation",
"requires": [
"step-agent-session",
"step-wiring"
],
"inputs": [
"rpc:agent-hardware-request",
"operation:op_m5-agent-0001",
"agentSession:agt_m5-0001",
"workerSession:wkr_m5-0001",
"resource:res_m5-control-relay",
"capability:cap_m5-relay-set",
"patchPanelStatus:pps_m5-0001"
],
"outputs": [
"op_m5-agent-0001",
"trc_m5-agent-0001"
],
"acceptanceStepIds": [
"agent-runtime"
]
},
{
"id": "step-evidence",
"order": 8,
"title": "evidence",
"kind": "evidence",
"requires": [
"step-direct-hardware-call",
"step-agent-hardware-call"
],
"inputs": [
"trace:trc_m5-direct-0001",
"trace:trc_m5-agent-0001",
"evidence:evi_m5-direct-0001",
"evidence:evi_m5-agent-0001"
],
"outputs": [
"evi_m5-direct-0001",
"evi_m5-agent-0001"
],
"acceptanceStepIds": [
"agent-runtime",
"artifact-observability"
]
},
{
"id": "step-cleanup",
"order": 9,
"title": "cleanup",
"kind": "cleanup",
"requires": [
"step-evidence"
],
"inputs": [
"cleanup:cln_m5-worker",
"cleanup:cln_m5-gateway-state",
"audit:aud_m5-cleanup"
],
"outputs": [
"cln_m5-worker",
"cln_m5-gateway-state"
],
"acceptanceStepIds": [
"agent-runtime"
]
}
],
"realDevGate": {
"sourcePlanCommand": "hwlab-cli test e2e --env dev --mvp",
"command": "hwlab-cli test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production",
"requiresAllBlockersClear": true,
"requiresDryRunPass": true,
"requiresArtifactObservability": true,
"requiresD601RouteObservation": true,
"requiresHumanApprovalForRealDev": true
},
"safety": {
"mode": "dry-run",
"allowNetwork": false,
"allowDeploy": false,
"allowBrowserE2E": false,
"allowSecrets": false,
"prohibitedActions": [
"real-dev-deploy",
"prod-deploy",
"prod-smoke",
"browser-e2e",
"heavyweight-e2e",
"secret-or-token-read",
"force-push",
"unidesk-runtime-substitution"
]
}
};
+101 -22
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HWLAB Cloud Web</title>
<title>HWLAB DEV MVP Gate</title>
<link rel="stylesheet" href="./styles.css" />
<script type="module" src="./app.mjs"></script>
</head>
@@ -11,47 +11,126 @@
<main class="shell">
<header class="topbar">
<div>
<p class="eyebrow">HWLAB</p>
<h1>Cloud Web</h1>
<p class="eyebrow">HWLAB L6 Frontend / CLI MVP</p>
<h1>DEV MVP Gate</h1>
</div>
<div class="status" id="status">dev skeleton</div>
<div class="status" id="status"></div>
</header>
<section class="panel">
<section class="panel compact">
<div class="panel-head">
<h2>Runtime</h2>
<p>Mock dashboard for the L6 CLI and cloud-web surface.</p>
<h2>Acceptance Source</h2>
<p>Contract-driven view of the checked-in M0-M5 gate report and M5 dry-run plan.</p>
</div>
<dl class="grid" id="runtime-grid"></dl>
<div class="metrics six" id="meta-grid"></div>
</section>
<section class="panel compact">
<div class="panel-head">
<h2>M0-M5 Gates</h2>
<p>Local contract and dry-run status. Live DEV remains separate from fixture evidence.</p>
</div>
<div class="milestones" id="milestone-grid"></div>
</section>
<section class="panel">
<div class="panel-head">
<h2>MVP route</h2>
<p>Fixed dry-run path shared with the CLI skeleton.</p>
</div>
<ol class="steps" id="mvp-steps"></ol>
</section>
<section class="panel">
<div class="panel-head">
<h2>Projects</h2>
<p>Static project list seeded from the mock fixture.</p>
<h2>DEV Health Contract</h2>
<p>Read from the M5 gate fixture; live health is not claimed while blockers are open.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Project</th>
<th>Component</th>
<th>Service</th>
<th>Status</th>
<th>Environment</th>
<th>Updated</th>
<th>Observed</th>
<th>Endpoint</th>
</tr>
</thead>
<tbody id="projects-body"></tbody>
<tbody id="health-body"></tbody>
</table>
</div>
</section>
<section class="panel">
<div class="panel-head">
<h2>Artifact / Deploy Commit</h2>
<p>Artifact identity fields required by the MVP DoD.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Service</th>
<th>Commit</th>
<th>Image</th>
<th>Digest</th>
<th>Health timestamp</th>
</tr>
</thead>
<tbody id="artifact-body"></tbody>
</table>
</div>
</section>
<section class="split">
<section class="panel compact">
<div class="panel-head">
<h2>2 Box / 2 Gateway / Patch Panel</h2>
<p>Topology contract for the hardware trusted loop.</p>
</div>
<div class="metrics two" id="topology-grid"></div>
</section>
<section class="panel compact">
<div class="panel-head">
<h2>Agent Session Evidence</h2>
<p>Agent and worker session IDs tied to trace, audit, evidence, and cleanup.</p>
</div>
<div class="metrics two" id="agent-grid"></div>
</section>
</section>
<section class="panel">
<div class="panel-head">
<h2>Evidence Records</h2>
<p>Direct and agent hardware operations remain fixture-bound until live DEV blockers clear.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Evidence</th>
<th>Operation</th>
<th>Service</th>
<th>URI</th>
<th>SHA-256</th>
</tr>
</thead>
<tbody id="evidence-body"></tbody>
</table>
</div>
</section>
<section class="split">
<section class="panel compact danger">
<div class="panel-head">
<h2>Blocked Evidence</h2>
<p>Open blocker evidence that prevents live DEV acceptance.</p>
</div>
<ul class="blockers" id="blocker-list"></ul>
</section>
<section class="panel compact">
<div class="panel-head">
<h2>Operator Commands</h2>
<p>Safe CLI entry points for manual acceptance.</p>
</div>
<ol class="commands" id="command-list"></ol>
</section>
</section>
</main>
</body>
</html>
+1 -1
View File
@@ -5,7 +5,7 @@ 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"]) {
for (const file of ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"]) {
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
}
+30 -6
View File
@@ -1,8 +1,13 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
import { gateSummary } from "../gate-summary.mjs";
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
const requiredFiles = ["index.html", "styles.css", "app.mjs", "runtime.mjs"];
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(rootDir, "../..");
const requiredFiles = ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"];
for (const file of requiredFiles) {
const filePath = path.resolve(rootDir, file);
@@ -12,8 +17,27 @@ for (const file of requiredFiles) {
}
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
if (!html.includes("HWLAB Cloud Web")) {
throw new Error("index.html missing title");
}
assert.match(html, /HWLAB DEV MVP Gate/);
assert.match(html, /health-body/);
assert.match(html, /artifact-body/);
assert.match(html, /agent-grid/);
console.log("hwlab-cloud-web check ok");
const sourceSummary = loadMvpGateSummary(repoRoot);
assert.equal(gateSummary.endpoint, sourceSummary.endpoint);
assert.equal(gateSummary.gateStatus, sourceSummary.gateStatus);
assert.equal(gateSummary.milestones.length, 6);
assert.deepEqual(
gateSummary.milestones.map((item) => item.id),
["M0", "M1", "M2", "M3", "M4", "M5"]
);
assert.equal(gateSummary.artifactCount, 13);
assert.equal(gateSummary.healthCount, 14);
assert.equal(gateSummary.gatewaySessionCount, 2);
assert.equal(gateSummary.boxResourceCount, 2);
assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.evidenceRecords.length, 2);
assert.equal(gateSummary.safety.allowNetwork, false);
console.log("hwlab-cloud-web check ok: gate summary reads M0-M5 report");
+222 -51
View File
@@ -1,13 +1,18 @@
:root {
color-scheme: dark;
--bg: #0d1117;
--surface: #161b22;
--surface-2: #1f2630;
--text: #e6edf3;
--muted: #9aa4b2;
--accent: #58a6ff;
--border: #30363d;
--good: #3fb950;
--bg: #101214;
--surface: #171a1f;
--surface-2: #20242b;
--surface-3: #252a32;
--text: #edf0e8;
--muted: #a9b0aa;
--line: #373d43;
--accent: #f0c75e;
--ok: #7fc97a;
--warn: #f0a35e;
--bad: #ff6b5f;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--body: "Aptos", "Segoe UI", system-ui, sans-serif;
}
* {
@@ -20,40 +25,41 @@ body {
min-height: 100%;
background: var(--bg);
color: var(--text);
font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font: 13px/1.45 var(--body);
}
body {
padding: 24px;
padding: 18px;
}
.shell {
max-width: 1160px;
max-width: 1380px;
margin: 0 auto;
display: grid;
gap: 16px;
gap: 12px;
}
.topbar,
.panel {
background: var(--surface);
border: 1px solid var(--border);
border: 1px solid var(--line);
}
.topbar {
min-height: 74px;
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 16px 18px;
padding: 14px 16px;
}
.eyebrow {
margin: 0 0 4px;
margin: 0 0 2px;
color: var(--muted);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0;
font-size: 12px;
text-transform: uppercase;
}
h1,
@@ -61,94 +67,259 @@ h2,
p,
dl,
ol,
ul,
table {
margin: 0;
}
h1 {
font-size: 22px;
font-size: 24px;
line-height: 1.1;
}
h2 {
font-size: 15px;
line-height: 1.2;
}
.status {
padding: 8px 12px;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--good);
display: flex;
align-items: center;
justify-content: flex-end;
}
.panel {
padding: 16px 18px;
padding: 14px 16px;
}
.panel.compact {
padding: 12px 14px;
}
.panel-head {
display: grid;
gap: 4px;
margin-bottom: 16px;
gap: 3px;
margin-bottom: 12px;
}
.panel-head p {
color: var(--muted);
max-width: 880px;
}
.grid {
.split {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
}
.grid dt {
color: var(--muted);
font-size: 12px;
}
.grid dd {
margin: 2px 0 0;
font-weight: 600;
word-break: break-word;
}
.steps {
.metrics {
display: grid;
gap: 8px;
padding-left: 20px;
}
.steps li {
padding-left: 8px;
.metrics.six {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.metrics.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric,
.milestone {
min-width: 0;
background: var(--surface-2);
border: 1px solid var(--line);
padding: 9px 10px;
}
.metric-label {
display: block;
color: var(--muted);
font-size: 11px;
line-height: 1.2;
}
.metric-value {
display: block;
margin-top: 4px;
font-size: 13px;
line-height: 1.25;
overflow-wrap: anywhere;
}
.milestones {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}
.milestone {
display: grid;
gap: 7px;
min-height: 118px;
}
.milestone-head,
.blocker-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.milestone p,
.blockers p {
color: var(--text);
overflow-wrap: anywhere;
}
.milestone small {
color: var(--muted);
}
.badge {
display: inline-flex;
align-items: center;
width: fit-content;
max-width: 100%;
min-height: 24px;
padding: 3px 8px;
border: 1px solid var(--line);
background: var(--surface-3);
color: var(--text);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
overflow-wrap: anywhere;
}
.tone-pass,
.tone-healthy {
color: var(--ok);
}
.tone-blocked,
.tone-network_blocker {
color: var(--bad);
}
.tone-not_run {
color: var(--warn);
}
.table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
}
table {
width: 100%;
min-width: 900px;
border-collapse: collapse;
background: var(--surface-2);
}
th,
td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--border);
vertical-align: top;
padding: 8px 9px;
border-bottom: 1px solid var(--line);
}
th {
color: var(--muted);
font-weight: 500;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
@media (max-width: 760px) {
td {
max-width: 360px;
overflow-wrap: anywhere;
}
tbody tr:last-child td {
border-bottom: 0;
}
.mono {
font-family: var(--mono);
font-size: 12px;
}
.wrap {
overflow-wrap: anywhere;
word-break: break-word;
}
.danger {
border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
}
.blockers,
.commands {
display: grid;
gap: 8px;
padding-left: 0;
list-style: none;
}
.blockers li,
.commands li {
min-width: 0;
padding: 9px 10px;
background: var(--surface-2);
border: 1px solid var(--line);
overflow-wrap: anywhere;
}
.commands {
counter-reset: command;
}
.commands li {
counter-increment: command;
}
.commands li::before {
content: counter(command) ". ";
color: var(--accent);
font-family: var(--body);
font-weight: 700;
}
@media (max-width: 1120px) {
.metrics.six,
.milestones {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 820px) {
body {
padding: 14px;
padding: 10px;
}
.topbar,
.split {
grid-template-columns: 1fr;
}
.topbar {
flex-direction: column;
align-items: flex-start;
display: grid;
align-items: start;
}
.grid {
.status {
justify-content: flex-start;
}
.metrics.six,
.metrics.two,
.milestones {
grid-template-columns: 1fr;
}
}