#!/usr/bin/env node import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const fixedNow = () => "2026-05-21T00:00:00.000Z"; const DEV_ENDPOINT = "http://74.48.78.17:16667"; const requiredDocs = ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"]; const reportPath = "reports/dev-gate/dev-m4-agent-loop.json"; const preflightCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"; const dryRunCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run"; function parseArgs(argv) { const flags = new Set(); for (const arg of argv) { if (arg.startsWith("--")) { flags.add(arg); } } return { live: flags.has("--live") || flags.has("--allow-live"), dryRun: flags.has("--dry-run"), confirmDev: flags.has("--confirm-dev"), confirmedNonProduction: flags.has("--confirmed-non-production") }; } async function readJSON(relativePath) { const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); return JSON.parse(raw); } async function readText(relativePath) { return readFile(path.join(repoRoot, relativePath), "utf8"); } async function writeJSON(relativePath, value) { const absolutePath = path.join(repoRoot, relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } function currentOriginMainCommitId() { return execFileSync("git", ["rev-parse", "--short=7", "origin/main"], { cwd: repoRoot, encoding: "utf8" }).trim(); } function requestJson(urlString, { method = "GET", headers = {}, body, timeoutMs = 5000 } = {}) { const url = new URL(urlString); const client = url.protocol === "https:" ? httpsRequest : httpRequest; const payload = body === undefined ? null : Buffer.from(body); return new Promise((resolve, reject) => { const request = client( { protocol: url.protocol, hostname: url.hostname, port: url.port, path: `${url.pathname}${url.search}`, method, headers: { ...headers } }, (response) => { let raw = ""; response.setEncoding("utf8"); response.on("data", (chunk) => { raw += chunk; }); response.on("end", () => { let parsed = null; if (raw.length > 0) { try { parsed = JSON.parse(raw); } catch (error) { reject(new Error(`invalid JSON response from ${urlString}: ${error.message}`)); return; } } resolve({ statusCode: response.statusCode ?? 0, headers: response.headers, body: parsed, raw }); }); } ); request.on("error", reject); request.setTimeout(timeoutMs, () => { request.destroy(new Error(`request to ${urlString} timed out after ${timeoutMs}ms`)); }); if (payload) { request.write(payload); } request.end(); }); } function statusForResult(result) { return result.blocked ? "blocked" : "pass"; } function blockerForResult(result) { return result.blocked ? [ { type: result.blockerType, scope: "devPreconditions", status: "open", summary: result.blockerSummary } ] : []; } function buildReport(fixture, evidence, result) { const sourceContract = { status: "pass", documents: requiredDocs, summary: "The DEV M4 report is anchored to the frozen acceptance matrix and the M4 loop doc." }; const validationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs", "node --check scripts/dev-m4-agent-loop-smoke.mjs", dryRunCommand, preflightCommand ]; return { $schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json", $id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m4-agent-loop.json", reportVersion: "v1", issue: "pikasTech/HWLAB#37", taskId: "dev-m4-agent-loop", commitId: currentOriginMainCommitId(), acceptanceLevel: "dev_m4_agent_loop", devOnly: true, prodDisabled: true, sourceContract, validationCommands, localSmoke: { status: "pass", commands: ["node scripts/m4-agent-loop-smoke.mjs"], evidence: [ "Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.", "Workspace isolation and explicit skills commit wiring are covered by fixture assertions." ], summary: "Local contract smoke passes on the repo fixture." }, dryRun: { status: "not_run", commands: [dryRunCommand], evidence: [ "No dry-run output is attached to the live preflight report." ], summary: "The live preflight report does not attempt a dry-run agent run." }, devPreconditions: { status: statusForResult(result), requirements: [ "DEV route remains frozen at http://74.48.78.17:16667", "No secret reads, real deployment, or long-running agent task is attempted", "Agent manager, worker, and skills are reachable only through the cloud API boundary" ], summary: result.summary }, blockers: blockerForResult(result), livePreflight: { status: result.blocked ? "blocked" : "pass", commands: [preflightCommand], evidence: evidence.length ? evidence : ["No live DEV observation was recorded."], summary: result.summary }, notes: "DEV M4 agent loop live preflight report. No secret material is read." }; } async function main() { const args = parseArgs(process.argv.slice(2)); assert.equal(args.dryRun, false, "--dry-run is not supported for this live preflight"); assert.equal(args.live, true, "live preflight requires --live"); assert.equal(args.confirmDev, true, "live preflight requires --confirm-dev"); assert.equal(args.confirmedNonProduction, true, "live preflight requires --confirmed-non-production"); const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json"); const evidence = []; const localEvidence = await readText(fixture.evidencePath); assert.ok(localEvidence.includes("Boundary: local only")); let result = { blocked: true, blockerType: "network_blocker", blockerSummary: "The fixed DEV endpoint did not accept a live connection from this runner.", summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is not reachable from this D601 runner.` }; try { const health = await requestJson(`${DEV_ENDPOINT}/health`); if (health.statusCode < 200 || health.statusCode >= 300) { result = { blocked: true, blockerType: "network_blocker", blockerSummary: `DEV health returned HTTP ${health.statusCode}.`, summary: `Blocked on DEV ingress health because ${DEV_ENDPOINT}/health did not return success.` }; } else { const body = health.body || {}; evidence.push(`health:${body.serviceId}:${body.environment}:${body.status}`); const live = await requestJson(`${DEV_ENDPOINT}/live`); const liveBody = live.body || {}; if (live.statusCode < 200 || live.statusCode >= 300 || liveBody?.serviceId !== "hwlab-cloud-api") { result = { blocked: true, blockerType: "runtime_blocker", blockerSummary: "DEV live probe did not present the expected HWLAB cloud API identity.", summary: "Blocked because the live DEV boundary did not expose the expected cloud API identity." }; } else { evidence.push(`live:${liveBody.serviceId}:${liveBody.status}`); const rpcHealth = await requestJson(`${DEV_ENDPOINT}/rpc`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: "req_dev_m4_health", method: "system.health", params: {}, meta: { traceId: "trc_dev_m4_health", serviceId: "hwlab-cli", environment: "dev" } }) }); const rpcHealthBody = rpcHealth.body || {}; evidence.push(`rpc-health:${rpcHealthBody.result?.serviceId ?? "unknown"}`); const rpcHardware = await requestJson(`${DEV_ENDPOINT}/rpc`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: "req_dev_m4_hw", method: "hardware.operation.request", params: { projectId: fixture.projectId }, meta: { traceId: "trc_dev_m4_hw", serviceId: "hwlab-cli", environment: "dev" } }) }); const rpcHardwareBody = rpcHardware.body || {}; const accepted = rpcHardwareBody.result?.accepted === true; const status = rpcHardwareBody.result?.status || rpcHardwareBody.error?.message || "unknown"; evidence.push(`rpc-hardware:${status}`); if (!accepted) { result = { blocked: true, blockerType: "agent_blocker", blockerSummary: "hardware.operation.request remains a skeleton response in the current DEV runtime.", summary: "Blocked by M3 readiness because the agent hardware dispatch path is still skeleton-only." }; } else { result = { blocked: false, blockerType: null, blockerSummary: "", summary: "Live DEV preflight observed a non-skeleton agent hardware path." }; } } } } catch (error) { result = { blocked: true, blockerType: "network_blocker", blockerSummary: error instanceof Error ? error.message : String(error), summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is unreachable from this runner.` }; } const report = buildReport(fixture, evidence, result); await writeJSON(reportPath, report); process.stdout.write(JSON.stringify({ reportPath, status: report.devPreconditions.status, blocker: report.blockers[0] ?? null, evidence: report.livePreflight.evidence, summary: report.livePreflight.summary, observedAt: fixedNow() }, null, 2)); process.stdout.write("\n"); } await main();