feat: sync g14 harness and device-pod cli

This commit is contained in:
Codex
2026-05-31 05:07:16 +08:00
parent 1ebbfe2bfe
commit 5bf4bf0f47
17 changed files with 2086 additions and 210 deletions
+249 -15
View File
@@ -61,7 +61,11 @@ function help() {
"device-pod-cli profile list --api-base-url URL --session-token TOKEN",
"device-pod-cli profile show --pod-id device-pod-71-freq --api-base-url URL --session-token TOKEN",
"device-pod-cli device-pod-71-freq:workspace:/ ls --api-base-url URL --session-token TOKEN",
"device-pod-cli device-pod-71-freq:workspace:/ put User/new.c --reason TEXT --lease-token TOKEN < file",
"device-pod-cli device-pod-71-freq:workspace:/ rmdir User/empty --reason TEXT --lease-token TOKEN",
"device-pod-cli device-pod-71-freq:workspace:/ keil add-source User/new.c --group User --reason TEXT --lease-token TOKEN",
"device-pod-cli device-pod-71-freq:workspace:/ build start --reason TEXT --lease-token TOKEN",
"device-pod-cli device-pod-71-freq:io-probe:/uart/1 jsonrpc gpio.read --params-json '{\"pin\":\"PB5\'} --reason TEXT --lease-token TOKEN",
"device-pod-cli job output --pod-id device-pod-71-freq <jobId> --api-base-url URL --session-token TOKEN",
"device-pod-cli lease acquire --pod-id device-pod-71-freq --reason TEXT --api-base-url URL --session-token TOKEN"
]
@@ -207,23 +211,117 @@ async function buildJob(selector: any, operation: string, rest: string[], parsed
let intent = "";
if (selector.surface === "workspace") {
const basePath = selector.path || ".";
if (operation === "ls") { intent = "workspace.ls"; args.path = joinPath(basePath, rest[1] || ""); }
else if (operation === "cat") { intent = "workspace.cat"; args.path = joinPath(basePath, rest[1] || ""); args.maxBytes = numberOption(parsed.maxBytes ?? parsed.limit); }
else if (operation === "rg") { intent = "workspace.rg"; args.pattern = requiredText(rest[1], "pattern"); args.path = joinPath(basePath, rest[2] || ""); Object.assign(args, passthroughOptions(parsed, ["glob", "g", "filesWithMatches", "l", "ignoreCase", "i", "maxCount"])); }
else if (operation === "apply-patch") { intent = "workspace.apply-patch"; args.path = joinPath(basePath, rest[1] || ""); args.patch = await patchText(parsed, stdinText); }
else if (operation === "build") { intent = "workspace.build"; args.action = rest[1] || "start"; Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs"])); }
if (operation === "ls") {
intent = "workspace.ls";
args.path = joinPath(basePath, rest[1] || "");
}
else if (operation === "cat") {
intent = "workspace.cat";
args.path = joinPath(basePath, rest[1] || "");
args.maxBytes = numberOption(parsed.maxBytes ?? parsed.limit);
}
else if (operation === "rg") {
intent = "workspace.rg";
args.pattern = requiredText(rest[1], "pattern");
args.path = joinPath(basePath, rest[2] || "");
Object.assign(args, passthroughOptions(parsed, [
"glob",
"g",
"filesWithMatches",
"l",
"ignoreCase",
"i",
"maxCount"
]));
}
else if (operation === "put") {
intent = "workspace.put";
args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path"));
Object.assign(
args,
await contentPayload(parsed, stdinText),
passthroughOptions(parsed, ["encoding", "charset", "createOnly", "createDirs", "updateOnly"])
);
}
else if (operation === "rm") {
intent = "workspace.rm";
args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path"));
Object.assign(args, passthroughOptions(parsed, ["missingOk"]));
}
else if (operation === "rmdir") {
intent = "workspace.rmdir";
args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path"));
}
else if (operation === "keil") {
intent = "workspace.keil";
args.action = requiredText(rest[1], "keil action");
args.base = basePath;
args.path = text(rest[2] ?? parsed.path ?? parsed.source ?? parsed.file ?? parsed.src);
Object.assign(args, passthroughOptions(parsed, ["group", "groupName", "target", "timeoutMs"]));
}
else if (operation === "apply-patch") {
intent = "workspace.apply-patch";
args.base = joinPath(basePath, rest[1] || "");
args.patch = await patchText(parsed, stdinText);
Object.assign(args, passthroughOptions(parsed, ["encoding", "charset", "createDirs"]));
}
else if (operation === "build") {
intent = "workspace.build";
args.action = rest[1] || "start";
Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs", "clean", "dryRun"]));
}
else throw cliError("unsupported_workspace_operation", `unsupported workspace operation: ${operation}`);
} else if (selector.surface === "debug-probe") {
if (operation === "chip-id") intent = "debug.chip-id";
else if (operation === "download") { intent = "debug.download"; args.action = rest[1] || "start"; Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs", "captureUart", "captureDurationMs", "durationMs", "port", "baudRate"])); }
else if (operation === "download") {
intent = "debug.download";
args.action = rest[1] || "start";
Object.assign(args, passthroughOptions(parsed, [
"target",
"timeoutMs",
"captureUart",
"captureDurationMs",
"durationMs",
"port",
"baudRate"
]));
}
else if (operation === "reset") intent = "debug.reset";
else throw cliError("unsupported_debug_operation", `unsupported debug-probe operation: ${operation}`);
} else if (selector.surface === "io-probe") {
const uartId = normalizeProbePath(selector.path || "uart/1");
if (operation === "ports") { intent = "io.ports"; args.uartId = uartId; }
else if (operation === "read") { intent = "io.uart.read"; args.uartId = uartId; Object.assign(args, passthroughOptions(parsed, ["durationMs", "port", "baudRate"])); }
else if (operation === "read-after-launch-flash") { intent = "io.uart.read-after-launch-flash"; args.uartId = uartId; Object.assign(args, passthroughOptions(parsed, ["durationMs", "port", "baudRate", "flashBase", "skipHardwareReset", "connectMode", "timeoutMs"])); }
else if (operation === "write") { intent = "io.uart.write"; args.uartId = uartId; args.message = text(rest[1] ?? parsed.message ?? parsed.text ?? parsed.data); args.hex = parsed.hex === true; Object.assign(args, passthroughOptions(parsed, ["port", "baudRate"])); }
else if (operation === "read") {
intent = "io.uart.read";
args.uartId = uartId;
Object.assign(args, passthroughOptions(parsed, ["durationMs", "port", "baudRate"]));
}
else if (operation === "read-after-launch-flash") {
intent = "io.uart.read-after-launch-flash";
args.uartId = uartId;
Object.assign(args, passthroughOptions(parsed, [
"durationMs",
"port",
"baudRate",
"flashBase",
"skipHardwareReset",
"connectMode",
"timeoutMs"
]));
}
else if (operation === "write") {
intent = "io.uart.write";
args.uartId = uartId;
args.message = text(rest[1] ?? parsed.message ?? parsed.text ?? parsed.data);
args.hex = parsed.hex === true;
Object.assign(args, passthroughOptions(parsed, ["port", "baudRate"]));
}
else if (operation === "jsonrpc") {
intent = "io.uart.jsonrpc";
args.uartId = uartId;
args.method = text(rest[1] ?? parsed.method);
Object.assign(args, jsonRpcArgs(parsed), passthroughOptions(parsed, uartJsonRpcOptionKeys));
}
else throw cliError("unsupported_io_operation", `unsupported io-probe operation: ${operation}`);
}
return clean({ intent, args: clean(args), reason: text(parsed.reason), leaseToken: text(parsed.leaseToken) });
@@ -259,18 +357,31 @@ function parseOptions(argv: string[]): ParsedArgs {
const out: ParsedArgs = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const item = argv[i] ?? "";
if (/^-[A-Za-z]$/u.test(item)) {
const key = item.slice(1);
const next = argv[i + 1];
if (next && !next.startsWith("-")) { setOption(out, key, next); i += 1; }
else setOption(out, key, true);
continue;
}
if (!item.startsWith("--")) { out._.push(item); continue; }
const eq = item.indexOf("=");
const rawKey = eq >= 0 ? item.slice(2, eq) : item.slice(2);
const key = rawKey.replace(/-([a-z])/gu, (_, c) => String(c).toUpperCase());
if (eq >= 0) { out[key] = item.slice(eq + 1); continue; }
if (eq >= 0) { setOption(out, key, item.slice(eq + 1)); continue; }
const next = argv[i + 1];
if (next && !next.startsWith("--")) { out[key] = next; i += 1; }
else out[key] = true;
if (next && !next.startsWith("--")) { setOption(out, key, next); i += 1; }
else setOption(out, key, true);
}
return out;
}
function setOption(out: ParsedArgs, key: string, value: unknown) {
if (out[key] === undefined) out[key] = value;
else if (Array.isArray(out[key])) (out[key] as unknown[]).push(value);
else out[key] = [out[key], value];
}
function parseSelector(raw: string) {
const match = raw.match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u);
return match ? { podId: match[1], surface: match[2], path: match[2] === "io-probe" ? normalizeProbePath(match[3] || "") : normalizePath(match[3] || ".") } : null;
@@ -286,14 +397,59 @@ function passthroughOptions(parsed: ParsedArgs, keys: string[]) {
return out;
}
const MUTATING_INTENTS = new Set(["workspace.apply-patch", "workspace.build", "debug.download", "debug.reset", "io.uart.read-after-launch-flash", "io.uart.write"]);
const MUTATING_INTENTS = new Set([
"workspace.apply-patch",
"workspace.build",
"workspace.put",
"workspace.rm",
"workspace.rmdir",
"workspace.keil",
"debug.download",
"debug.reset",
"io.uart.read-after-launch-flash",
"io.uart.write",
"io.uart.jsonrpc"
]);
const uartJsonRpcOptionKeys = [
"id",
"request",
"requestB64",
"params",
"paramsB64",
"responseTimeoutMs",
"durationMs",
"retry",
"retries",
"retryDelayMs",
"lineEnding",
"noNewline",
"lineDelimited",
"discardBefore",
"requireResponse",
"requireJson",
"requireJsonrpc",
"requireJsonrpcResult",
"allowIdMismatch",
"port",
"baudRate"
];
function defaultOperation(surface: string) { return surface === "workspace" ? "ls" : surface === "debug-probe" ? "status" : "read"; }
function apiBaseUrl(parsed: ParsedArgs, env: EnvLike) { const value = text(parsed.apiBaseUrl ?? parsed.apiUrl ?? env.HWLAB_DEVICE_POD_API_URL ?? env.HWLAB_CLOUD_API_URL); if (!value) throw cliError("api_base_url_required", "device-pod-cli requires --api-base-url or HWLAB_DEVICE_POD_API_URL/HWLAB_CLOUD_API_URL"); return value.replace(/\/+$/u, ""); }
function authHeaders(parsed: ParsedArgs, env: EnvLike) { const cookie = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE); const sessionToken = text(parsed.sessionToken ?? env.HWLAB_DEVICE_POD_SESSION_TOKEN ?? env.HWLAB_CLOUD_API_SESSION_TOKEN ?? env.HWLAB_SESSION_TOKEN); const bearer = text(parsed.bearerToken ?? env.HWLAB_BEARER_TOKEN); return clean({ ...(cookie ? { cookie: cookie.includes("=") ? cookie : `hwlab_session=${encodeURIComponent(cookie)}` } : {}), ...(sessionToken ? { "x-hwlab-session-token": sessionToken } : {}), ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) }); }
function responsePayload(action: string, response: any, extra: Record<string, unknown> = {}) { const success = response.status >= 200 && response.status < 300 && response.body?.ok !== false; return { ok: success, action, status: success ? "succeeded" : "failed", httpStatus: response.status, ...extra, body: response.body }; }
function withMeta(payload: any, now: () => string) { return { generatedAt: now(), cli: CLI_NAME, version: VERSION, ...payload }; }
function ok(action: string, data: Record<string, unknown> = {}, status = "succeeded") { return { ok: true, action, status, ...data }; }
function failure(action: string, error: any) { return { ok: false, action, status: "failed", error: errorSummary(error), ...(error?.details ? { details: error.details } : {}) }; }
function failure(action: string, error: any) {
const summary = errorSummary(error);
return {
ok: false,
action,
status: "failed",
error: summary,
...patchHintForText(summary.message),
...(error?.details ? { details: error.details } : {})
};
}
function cliError(code: string, message: string, details: Record<string, unknown> = {}) { return Object.assign(new Error(message), { code, details }); }
function errorSummary(error: any) { return { code: error?.code ?? "device_pod_cli_error", message: error?.message ?? String(error), ...(error?.details ? { details: error.details } : {}) }; }
function requiredOption(parsed: ParsedArgs, env: EnvLike, key: string, envKey: string) { return requiredText(parsed[key] ?? env[envKey], key); }
@@ -301,6 +457,7 @@ function requiredText(value: unknown, field: string) { const valueText = text(va
function text(value: unknown) { return String(value ?? "").trim(); }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
function clean<T extends Record<string, any>>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== false)) as T; }
function arrayOption(value: unknown) { return (Array.isArray(value) ? value : value === undefined ? [] : [value]).map((item) => text(item)).filter(Boolean); }
function devicePodSeedFromOptions(parsed: ParsedArgs, { required = false } = {}) {
const devicePodText = text(parsed.devicePodJson ?? parsed.devicePod);
if (devicePodText) return normalizeDevicePodSeed(parseJsonObjectOption(devicePodText, "devicePodJson"), "devicePodJson");
@@ -336,6 +493,83 @@ function normalizePath(value: string) { return String(value || ".").trim().repla
function normalizeProbePath(value: string) { const normalized = normalizePath(value); if (normalized === ".") return "uart/1"; if (normalized === "uart") throw cliError("invalid_io_probe_path", "io-probe path must be concrete, for example /uart/1"); return normalized; }
function joinPath(base: string, child: string) { const left = normalizePath(base); const right = normalizePath(child || "."); if (!right || right === ".") return left; if (child.startsWith("/")) return right; return normalizePath(`${left}/${right}`); }
async function patchText(parsed: ParsedArgs, stdinText?: string) { if (typeof parsed.patch === "string") return parsed.patch; if (typeof parsed.patchB64 === "string") return Buffer.from(parsed.patchB64, "base64").toString("utf8"); if (stdinText !== undefined) return stdinText; const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); const textValue = Buffer.concat(chunks).toString("utf8"); if (!textValue.trim()) throw cliError("patch_text_required", "workspace apply-patch requires patch text on stdin or --patch/--patch-b64"); return textValue; }
function redactJobRequest(job: any) { return { ...job, args: job.args?.patch ? { ...job.args, patch: undefined, patchBytes: Buffer.byteLength(job.args.patch, "utf8") } : job.args }; }
async function contentPayload(parsed: ParsedArgs, stdinText?: string) {
if (typeof parsed.contentB64 === "string") return { contentB64: parsed.contentB64 };
if (typeof parsed.textB64 === "string") return { textB64: parsed.textB64 };
if (typeof parsed.text === "string" || typeof parsed.content === "string") {
return { text: String(parsed.text ?? parsed.content) };
}
const content = stdinText !== undefined ? stdinText : await readStdinText();
if (!content) {
throw cliError(
"content_required",
"workspace put requires stdin text or --content-b64/--text/--text-b64"
);
}
return { contentB64: Buffer.from(content, "utf8").toString("base64") };
}
async function readStdinText() { const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8"); }
function jsonRpcArgs(parsed: ParsedArgs) {
return clean({
request: parsed.requestJson,
params: parsed.paramsJson ?? parsed.params,
expectResultField: arrayOption(parsed.expectResultField ?? parsed.resultField)
});
}
function redactJobRequest(job: any) { return { ...job, args: redactArgs(job.args) }; }
function redactArgs(args: any = {}) {
if (!args || typeof args !== "object") return args;
const next = { ...args };
if (typeof next.patch === "string") {
next.patch = undefined;
next.patchBytes = Buffer.byteLength(args.patch, "utf8");
}
if (typeof next.contentB64 === "string") {
next.contentB64 = undefined;
next.contentBytes = Buffer.from(args.contentB64, "base64").length;
}
if (typeof next.text === "string") {
next.text = undefined;
next.textBytes = Buffer.byteLength(args.text, "utf8");
}
return next;
}
function patchHintForText(message: unknown) {
const textValue = String(message ?? "");
const patchLike = /apply-patch|patch|hunk|\*\*\* Begin Patch|\*\*\* End Patch|Update File|Add File|Delete File/iu;
if (!patchLike.test(textValue)) return {};
return {
patchHint: {
standardForm: [
"*** Begin Patch",
"*** Update File: path/to/file.c",
"@@",
" exact unchanged context line",
"-old line",
"+new line",
"*** End Patch"
],
next: patchHintNext(textValue)
}
};
}
function patchHintNext(message: string) {
const next = [
"Re-read the current target file with workspace cat/rg, then retry a smaller exact-context hunk patch."
];
if (/end with \*\*\* End Patch/iu.test(message)) {
next.unshift("Add a final exact `*** End Patch` line.");
} else if (/start with \*\*\* Begin Patch/iu.test(message)) {
next.unshift("Start the patch with an exact `*** Begin Patch` line.");
} else if (/hunk did not match|must start with @@|invalid update line prefix|ellipsis/iu.test(message)) {
next.unshift("Use exact current context; do not use ellipsis or line-number-only hunks.");
} else if (/unsupported patch header|capitalization|New File/iu.test(message)) {
next.unshift("Use exactly `*** Update File:`, `*** Add File:`, or `*** Delete File:` headers.");
}
next.push("Do not switch to workspace put for existing source unless a smaller hunk cannot safely express the edit.");
return next;
}
function parseJson(value: string) { if (!value) return null; try { return JSON.parse(value); } catch { return { rawText: value.slice(0, 2000), parseError: true }; } }
function compactBody(body: any) { if (!body || typeof body !== "object") return body; return { ok: body.ok, status: body.status, authenticated: body.authenticated, actor: body.actor, contractVersion: body.contractVersion, devicePodCount: Array.isArray(body.devicePods) ? body.devicePods.length : undefined, selectedDevicePodId: body.selectedDevicePodId, error: body.error, summary: body.summary }; }
+298 -2
View File
@@ -57,6 +57,7 @@ async function clientCommand(context: any) {
if (group === "device-pods" || group === "device-pod") return devicePodsCommand(next);
if (group === "runtime") return runtimeCommand(next);
if (group === "agent") return agentCommand(next);
if (["harness", "harness-ops", "harness-opt"].includes(group)) return harnessCommand(next);
if (group === "workbench") return workbenchCommand(next);
if (group === "rpc") return rpcCommand(next);
if (group === "request") return requestCommand(next);
@@ -84,7 +85,10 @@ function help() {
"hwlab-cli client rpc system.health [--full]",
"hwlab-cli client agent send --message TEXT --provider-profile deepseek --timeout-ms 120000",
"hwlab-cli client agent trace TRACE_ID",
"hwlab-cli client agent cancel TRACE_ID"
"hwlab-cli client agent cancel TRACE_ID",
"hwlab-cli client harness submit --message TEXT --provider-profile deepseek",
"hwlab-cli client harness wait TRACE_ID --timeout-ms 55000",
"hwlab-cli client harness audit TRACE_ID --require-bootsharp"
]
});
}
@@ -277,6 +281,172 @@ async function agentCommand(context: any) {
throw cliError("unsupported_agent_command", `unsupported agent command: ${subcommand}`, { subcommand });
}
async function harnessCommand(context: any) {
const subcommand = context.rest[0] || "help";
if (["help", "--help", "-h"].includes(subcommand)) return harnessHelp();
if (subcommand === "health") {
const response = await requestJson({ ...context, method: "GET", path: "/health/live", auth: false });
return responsePayload("client.harness.health", response, context, {
route: route("GET", "/health/live"),
body: responseBodyForCli(response.body, context.parsed)
});
}
if (subcommand === "submit") return harnessSubmit({ ...context, rest: context.rest.slice(1) });
if (subcommand === "result") return harnessResult({ ...context, rest: context.rest.slice(1) });
if (subcommand === "trace") return harnessTrace({ ...context, rest: context.rest.slice(1) });
if (subcommand === "wait") return harnessWait({ ...context, rest: context.rest.slice(1) });
if (subcommand === "audit") return harnessAudit({ ...context, rest: context.rest.slice(1) });
throw cliError("unsupported_harness_command", `unsupported harness command: ${subcommand}`, { subcommand });
}
function harnessHelp() {
return ok("client.harness.help", {
serviceRuntime: false,
imagePublished: false,
aliases: ["harness", "harness-ops", "harness-opt"],
commands: [
"health [--base-url URL]",
"submit --message TEXT|--message-file PATH [--conversation-id ID] [--trace-id ID] [--provider-profile deepseek|codex-api]",
"result TRACE_ID [--full]",
"trace TRACE_ID [--limit N] [--full]",
"wait TRACE_ID [--timeout-ms N] [--poll-interval-ms N]",
"audit TRACE_ID|--trace-file PATH [--strict] [--require-bootsharp]"
]
});
}
async function harnessSubmit(context: any) {
const message = await messageValue(context);
if (!message.trim()) {
throw cliError(
"message_required",
"client harness submit requires --message, --message-file, or stdin text"
);
}
const traceId = text(context.parsed.traceId) || makeId("trc");
const conversationId = text(context.parsed.conversationId) || makeId("cnv");
const body = clean({
message,
conversationId,
sessionId: text(context.parsed.sessionId),
threadId: text(context.parsed.threadId),
traceId,
providerProfile: text(context.parsed.providerProfile) || "deepseek",
timeoutMs: numberOption(context.parsed.agentTimeoutMs ?? context.parsed.timeoutMs),
hardTimeoutMs: numberOption(context.parsed.hardTimeoutMs),
gatewayShellTimeoutMs: numberOption(context.parsed.gatewayShellTimeoutMs),
shortConnection: true,
projectId: text(context.parsed.projectId) || "prj_harness_ops"
});
const response = await requestJson({
...context,
method: "POST",
path: "/v1/agent/chat",
body,
timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS,
extraHeaders: {
"x-trace-id": traceId,
prefer: "respond-async",
"x-hwlab-short-connection": "1"
}
});
return responsePayload("client.harness.submit", response, context, {
route: route("POST", "/v1/agent/chat"),
traceId,
conversationId,
accepted: response.body?.accepted ?? false,
resultUrl: response.body?.resultUrl ?? `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
traceUrl: response.body?.traceUrl ?? `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function harnessResult(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.harness.result", response, context, {
route: route("GET", pathName),
traceId,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function harnessTrace(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
const body = context.parsed.full === true
? response.body
: compactTraceBody(response.body, numberOption(context.parsed.limit) ?? 24);
return responsePayload("client.harness.trace", response, context, {
route: route("GET", pathName),
traceId,
body
});
}
async function harnessWait(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const timeoutMs = Math.min(numberOption(context.parsed.timeoutMs) ?? 55000, 60000);
const pollIntervalMs = Math.max(numberOption(context.parsed.pollIntervalMs) ?? 1000, 250);
const deadline = Date.now() + timeoutMs;
const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
let polls = 0;
let lastResponse: any = null;
while (Date.now() < deadline) {
polls += 1;
lastResponse = await requestJson({
...context,
method: "GET",
path: pathName,
timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000)
});
if (lastResponse.status === 200 && lastResponse.body?.status && lastResponse.body.status !== "running") {
return responsePayload("client.harness.wait", lastResponse, context, {
route: route("GET", pathName),
traceId,
polls,
timedOut: false,
body: responseBodyForCli(lastResponse.body, context.parsed)
});
}
await context.sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
}
return ok("client.harness.wait", {
status: "timeout",
traceId,
route: route("GET", pathName),
polls,
timedOut: true,
timeoutMs,
body: lastResponse ? responseBodyForCli(lastResponse.body, context.parsed) : null
}, "timeout");
}
async function harnessAudit(context: any) {
let traceObject: any;
if (typeof context.parsed.traceFile === "string") {
traceObject = JSON.parse(await readFile(path.resolve(context.cwd, context.parsed.traceFile), "utf8"));
} else {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const response = await requestJson({
...context,
method: "GET",
path: `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`
});
traceObject = response.body;
}
const report = auditTrace(traceObject, { requireBootsharp: context.parsed.requireBootsharp === true });
const status = context.parsed.strict === true && report.frictionSignals.length > 0
? "failed"
: "succeeded";
return ok("client.harness.audit", {
traceId: traceObject?.traceId ?? context.rest[0] ?? null,
...report
}, status);
}
async function agentSend(context: any) {
const { parsed } = context;
const message = text(parsed.message ?? parsed.text) || text(context.stdinText);
@@ -444,6 +614,13 @@ async function rpcParamsValue(context: any) {
}
}
async function messageValue(context: any) {
if (typeof context.parsed.message === "string") return context.parsed.message;
if (typeof context.parsed.text === "string") return context.parsed.text;
if (typeof context.parsed.messageFile === "string") return await readFile(path.resolve(context.cwd, context.parsed.messageFile), "utf8");
return text(context.stdinText);
}
async function requestBodyValue(context: any) {
const { parsed } = context;
const raw = typeof parsed.bodyJson === "string"
@@ -661,6 +838,110 @@ function compactAssistantStream(value: any) {
return pruneUndefined({ status: value.status, chunkCount: value.chunkCount, text: value.text, lastChunk: value.lastChunk, updatedAt: value.updatedAt });
}
function compactTraceBody(traceObject: any, limit: number) {
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
const tail = events.slice(-limit).map((event: any) => ({
at: event.createdAt ?? event.timestamp ?? null,
label: event.label ?? null,
status: event.status ?? null,
waitingFor: event.waitingFor ?? null,
message: preview(event.message ?? event.command ?? event.error ?? null, 240)
}));
return pruneUndefined({
traceId: traceObject?.traceId ?? null,
status: traceObject?.status ?? null,
waitingFor: traceObject?.waitingFor ?? traceObject?.lastEvent?.waitingFor ?? null,
eventCount: traceObject?.eventCount ?? events.length,
lastEvent: traceObject?.lastEvent ?? events.at(-1) ?? null,
eventTail: tail,
fullBodyAvailable: true
});
}
function auditTrace(traceObject: any, { requireBootsharp = false } = {}) {
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
if (traceObject?.status === "missing" && events.length === 0) {
return {
status: "trace_missing",
summary: { hasHwpod: false, hasBootsharp: false, hasApplyPatch: false, frictionCount: 0 },
frictionSignals: [],
recommendation: "Trace is missing. Check the traceId and poll result/trace after submit."
};
}
const textValue = commandTextFromEvents(events);
const signals = [
signal(
"low_level_tran",
/\/app\/tools\/(?:hwlab-gateway-)?tran\.mjs/u,
textValue,
"Use named device-pod operations through the CLI instead of low-level transport."
),
signal(
"long_device_pod_cli_path",
/node\s+\/app\/(?:tools\/device-pod-cli\.mjs|skills\/device-pod-cli\/scripts\/device-pod-cli\.mjs)/u,
textValue,
"Prefer the main hwlab-cli client entry or short documented device-pod command."
),
signal(
"temporary_script_workaround",
temporaryScriptPattern,
textValue,
"Add or use a named device-pod operation instead of staging temporary scripts."
),
signal(
"workspace_put_text_edit",
/(?:\S*workspace:\/\S*\s+put\s+(?!status\b)\S+|\bworkspace\s+put\s+(?!status\b)\S+)/u,
textValue,
"Workspace put is available, but source text edits should prefer apply-patch unless whole-file write is intentional."
),
signal(
"status_shell_parsing",
statusShellParsingPattern,
textValue,
"Use compact JSON fields returned by harness result/trace instead of shell parsing."
),
requireBootsharp && !/\bbootsharp\b/u.test(textValue)
? {
kind: "missing_bootsharp",
count: 1,
examples: [],
hint: "No bootsharp/bootstrap device-pod context call was found in retained trace events."
}
: null
].filter(Boolean);
return {
status: signals.length > 0 ? "friction_detected" : "clean",
summary: {
hasHwpod: /\bhwpod\b/u.test(textValue),
hasBootsharp: /\bbootsharp\b/u.test(textValue),
hasApplyPatch: /\bapply-patch\b/u.test(textValue),
frictionCount: signals.length
},
frictionSignals: signals,
recommendation: signals.length > 0
? "Treat these as tool-friction signals and improve named CLI/device-pod operations; this is not a runtime gate."
: "No obvious device-pod transport friction in retained trace events."
};
}
const temporaryScriptPattern = /(?:cat\s*>\s*\S+\.(?:py|ps1|js|mjs|bat)\b|upload\b[^\n]*\.(?:py|ps1|js|mjs|bat)\b|(?:python|powershell|pwsh)\s+\S+\.(?:py|ps1)\b)/iu;
const statusShellParsingPattern = /(?:device-pod-cli|hwpod|hwlab-cli)\b[^\n]*(?:\bstatus\b|\btrace\b)[^\n]*(?:\|\s*(?:grep|head|python3?\s+-c|jq)\b|;\s*done)/iu;
function commandTextFromEvents(events: any[]) {
return events.map((event: any) => {
if (typeof event?.command === "string") return event.command;
const label = String(event?.label ?? "");
const isToolEvent = event?.type === "tool_call" || event?.stage === "tool_call" || label.includes("commandExecution");
return isToolEvent && typeof event?.message === "string" ? event.message : null;
}).filter(Boolean).join("\n");
}
function signal(kind: string, pattern: RegExp, textValue: string, hint: string) {
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
const matches = [...textValue.matchAll(new RegExp(pattern.source, flags))];
return matches.length > 0 ? { kind, count: matches.length, examples: matches.slice(0, 3).map((match) => preview(match[0], 220)), hint } : null;
}
function compactRpcBody(body: any) {
return pruneUndefined({
jsonrpc: body.jsonrpc,
@@ -714,7 +995,9 @@ function safeUser(value: any) {
}
function withMeta(payload: any, now: () => string) { return { generatedAt: now(), cli: CLI_NAME, version: VERSION, ...payload }; }
function ok(action: string, data: Record<string, unknown> = {}, status = "succeeded") { return { ok: status !== "timeout", action, status, ...data }; }
function ok(action: string, data: Record<string, unknown> = {}, status = "succeeded") {
return { ok: !["failed", "timeout"].includes(status), action, status, ...data };
}
function failure(action: string, error: any) { return { ok: false, action, status: "failed", error: errorSummary(error), ...(error?.details ? { details: error.details } : {}) }; }
function errorSummary(error: any) { return { code: error?.code ?? "hwlab_cli_error", message: error?.message ?? String(error), ...(error?.details ? { details: error.details } : {}) }; }
function cliError(code: string, message: string, details: Record<string, unknown> = {}) { return Object.assign(new Error(message), { code, details }); }
@@ -729,3 +1012,16 @@ function pruneUndefined<T extends Record<string, any>>(value: T): T { return Obj
function parseJson(value: string) { if (!value) return null; try { return JSON.parse(value); } catch { return { rawText: value.slice(0, 2000), parseError: true }; } }
function makeId(prefix: string) { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; }
function wait(ms: number) { return new Promise<void>((resolve) => setTimeout(resolve, ms)); }
function requiredTraceId(value: unknown) {
const result = requiredText(value, "traceId");
if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(result)) {
throw cliError("invalid_trace_id", "traceId must start with trc_", { traceId: result });
}
return result;
}
function preview(value: unknown, max = 500) {
if (value === undefined || value === null) return null;
const result = String(value).replace(/\s+/gu, " ").trim();
return result.length > max ? `${result.slice(0, max)}...` : result;
}