case-run: archive artifacts to registry

This commit is contained in:
Codex Agent
2026-06-06 16:24:02 +08:00
parent 1d3ed4ac10
commit 4105072338
3 changed files with 226 additions and 21 deletions
+3 -3
View File
@@ -222,7 +222,7 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run result <runId>
bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs <runId> --tail 8000
```
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 采集 agent trace/terminal state/commandExecution 摘要、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build``cmd.run` Keil 异步 job-status 查询,最后收集 `evidence.json` 到 run state,并把审计副本写入 case registry repo 的 `runs/<caseId>/<runId>/`。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 采集 agent trace/terminal state/commandExecution 摘要、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build``cmd.run` Keil 异步 job-status 查询,最后 `evidence.json``summary.md``run.json``result.json``agent-prompt.md``agent-diff.patch`、run-local `.hwlab/hwpod-spec.yaml`、worker stdout/stderr 和 `artifact-manifest.json` 归档到 case registry repo 的 `runs/<caseId>/<runId>/`。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。
长时间 CaseRun 必须优先使用 `case run start/status/result/logs` 短连接形态,而不是用 shell/nohup 包装同步 `case run``start` 只负责创建 `.state/hwlab-cli/caserun/<runId>/`、启动后台 worker 并立即返回 `runId`、PID、stdout/stderr 路径和下一条查询命令;`status``result``logs` 都是短查询。`run.json` 是阶段状态权威,阶段开始时就要刷新 `status``stage``traceId/sessionId/conversationId/threadId`、AgentRun/trace 轮询信息、stdout/stderr byte count 和下一条轮询命令;长等待期间不得出现无输出、无 trace、无状态文件更新的黑洞窗口。
@@ -230,7 +230,7 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs <runId> --tail 8000
产物归属按两条线分开:
- case registry repo 的 `runs/<caseId>/<runId>/`审计产物,PR 默认保持打开,不作为必须合并的训练改进
- case registry repo 的 `runs/<caseId>/<runId>/` CaseRun 审计产物权威入口;`.state/hwlab-cli/caserun/<runId>/` 只保留短连接控制和轮询所需状态,不作为长期产物入口。registry run 目录必须包含 trace manifestmanifest 只记录文件哈希、agent trace/session、subject provenance、diff 和 Keil job 字段,不引入自动评价或门禁结论
- HWLAB repo 的 harness、`hwpod-cli``hwpod-ctl``hwpod-compiler-cli` 或文档改进可以按风险进入 `v0.2` 分支;单纯文档和轻量 CLI/helper 变更可直接提交,业务代码、运行面或发布链路变更走 PR 工作流。
当前边界:CaseRun 仍是无服务化短连接 CLI 编排,不负责排队、并发、评分、自动合并、长期 run retention 或 epoch 汇总;这些能力属于后续强化学习 Harness 层。CaseRun 也不负责代替 HWLAB Code Agent 完成研发任务,后续 agent-task CaseRun 必须把 prompt/session/trace/diff/evidence 作为一等输出。AgentRun 私有 GitHub repo git transport 必须具备有界失败和可观测性后,才能把单次 CaseRun 扩展为批量 runner 或 epoch 系统。
@@ -244,7 +244,7 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs <runId> --tail 8000
3. `hwpod-cli --dry-run` 输出的 plan 不需要云端 spec,也不读取其他本地旧 profile authority。
4. `hwlab-api` 的 node-ops 入口能接收 plan,返回 JSON result;无可用 node 时必须返回结构化 blocker,而不是静默伪造 DEV-LIVE。
5. 真实 CLI 验收必须证明 `hwpod-cli -> hwpod-compiler-cli -> hwlab-api /v1/hwpod-node-ops -> hwpod-node` 全链路走通。
6. CaseRun 流程必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath``subject.commitId` 创建隔离 subject worktree,生成 Code Agent prompt/context,记录 agent session/trace/conversation/provider,采集 subject worktree diff,执行 compile-only smoke,并产出 isolated run state 和 case registry repo evidence;当前版本只记录流程证据,不设置通过条件、不自动评价 evidence,并明确记录 downloadSkipped。
6. CaseRun 流程必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath``subject.commitId` 创建隔离 subject worktree,生成 Code Agent prompt/context,记录 agent session/trace/conversation/provider,采集 subject worktree diff,执行 compile-only smoke,并产出 isolated run state 和 case registry repo 全量产物归档;当前版本只记录流程证据和 trace manifest,不设置通过条件、不自动评价 evidence,并明确记录 downloadSkipped。
7. 长任务验收必须覆盖 `hwlab-cli case run start <caseId>` 立即返回,以及 `status/result/logs <runId>` 在 worker 运行中能短查询到阶段、耗时、stdout/stderr byte count、trace/session/conversation/thread 信息和下一条轮询命令;不得把同步命令长时间无输出视为有效 CaseRun 操作体验。
8. `d601-f103-v2-compile` 的真实流程只覆盖当前编排链路和 compile-only smoke;操作员直接运行 `hwpod-cli build`、直接调用 Keil 或只检查 node 侧 job,不等同于 CaseRun 流程跑完。
9. agent-task CaseRun 的真实流程必须证明 `case run` 已创建或调用 Code Agent / AgentRun session,任务 prompt 已进入 agent 上下文,CaseRun 已采集隔离 subject worktree diff,并已继续执行编译、下载或 I/O smoke 等后续动作;当前版本只记录这些事实,不自动判断 agent 是否完成任务。
+36 -2
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
@@ -223,7 +223,8 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
cwd,
now: () => "2026-06-05T00:00:00.000Z",
sleep: async () => {
const runState = JSON.parse(await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "run.json"), "utf8"));
const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow");
const runState = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf8"));
assert.equal(runState.stage, "agent-running");
assert.equal(runState.agent.stageStatus, "running");
assert.equal(runState.agent.sessionId, "ses_case_run");
@@ -232,6 +233,8 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
assert.match(runState.agent.traceId, /^trc_case_/u);
assert.match(runState.agent.nextPollCommand, /client agent result trc_case_/u);
assert.equal(runState._control.stage, "agent-running");
await writeFile(path.join(runDir, "worker.stdout.log"), "worker accepted\n", "utf8");
await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8");
sawAgentRunningState = true;
},
env: { HWLAB_API_KEY: "hwl_live_test_case_agent_key" },
@@ -267,6 +270,37 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
assert.equal(result.payload.collect.autoEvaluation, false);
assert.equal(sawAgentRunningState, true);
const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", "run-agent-flow");
const registryFiles = [
"evidence.json",
"summary.md",
"run.json",
"result.json",
"agent-prompt.md",
"agent-diff.patch",
path.join(".hwlab", "hwpod-spec.yaml"),
"worker.stdout.log",
"worker.stderr.log",
"artifact-manifest.json"
];
for (const file of registryFiles) await stat(path.join(registryRunDir, file));
const manifest = JSON.parse(await readFile(path.join(registryRunDir, "artifact-manifest.json"), "utf8"));
assert.equal(manifest.contractVersion, "hwpod-case-run-artifact-manifest-v1");
assert.equal(manifest.trace.traceId, result.payload.agent.traceId);
assert.equal(manifest.trace.sessionId, "ses_case_run");
assert.equal(manifest.trace.conversationId, "cnv_case_d601-f103-v2-compile_run-agent-flow");
assert.equal(manifest.trace.agentRun.runId, "run_agent_trace");
assert.equal(manifest.prompt.sha256, result.payload.evidence.agent.promptSha256);
assert.equal(manifest.diff.sha256, result.payload.evidence.agentDiff.diffPatchSha256);
assert.equal(manifest.decisions.autoEvaluation, false);
assert.equal(manifest.files.some((item: any) => item.path === "worker.stdout.log"), true);
assert.equal(manifest.files.some((item: any) => item.path === ".hwlab/hwpod-spec.yaml"), true);
assert.equal(result.payload.collect.caseRepoRunDir, registryRunDir);
assert.equal(result.payload.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json"));
assert.equal(result.payload.collect.caseRepoFiles.some((item: string) => item.endsWith("artifact-manifest.json")), true);
const registryResult = JSON.parse(await readFile(path.join(registryRunDir, "result.json"), "utf8"));
assert.equal(registryResult.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json"));
const prompt = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-prompt.md"), "utf8");
assert.match(prompt, /without auto-grading/u);
assert.match(prompt, /Run-local HWPOD spec/u);
+187 -16
View File
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { closeSync, openSync } from "node:fs";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
@@ -140,6 +140,22 @@ type AgentTraceStage = {
error?: Record<string, unknown> | null;
};
type CaseRegistryArchiveFile = {
path: string;
bytes: number;
sha256: string;
source?: string;
};
type CaseRegistryArchive = {
caseRepoRunDir: string;
artifactManifestPath: string;
caseRepoFiles: string[];
files: CaseRegistryArchiveFile[];
skippedFiles: string[];
manifest: Record<string, unknown>;
};
export async function caseCommand(context: CaseContext) {
const subcommand = context.rest[0] || "help";
if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp();
@@ -198,8 +214,13 @@ export async function runCaseRun(context: CaseContext) {
const diffStage = await collectAgentDiff(context, traceStage.run);
const built = await buildCaseRun(context, diffStage.run);
const collected = await collectCaseRun(context, built.run, built.evidence);
await writeRunControl(context, collected.run, { status: "completed", stage: "completed", completedAt: context.now(), resultPath: path.join(collected.run.runDir, "result.json") });
await writeJson(path.join(collected.run.runDir, "result.json"), compactCaseRunResult(collected));
const resultPath = path.join(collected.run.runDir, "result.json");
const completed = await writeRunControl(context, collected.run, { status: "completed", stage: "completed", completedAt: context.now(), resultPath });
await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run }));
const finalArchive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, completed.run, collected.evidence);
const finalCollect = finalArchive ? collectSummaryFromArchive(completed.run, collected.evidence, finalArchive) : collected.summary;
await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run, summary: finalCollect }));
if (context.parsed.noCaseRepoRecord !== true) await archiveCaseRunArtifacts(context, completed.run, collected.evidence);
return ok("case.run", {
caseId: collected.run.caseId,
runId: collected.run.runId,
@@ -210,7 +231,7 @@ export async function runCaseRun(context: CaseContext) {
agentTrace: traceSummary(traceStage.trace),
agentDiff: diffSummary(diffStage.diff),
build: built.summary,
collect: collected.summary,
collect: finalCollect,
evidence: collected.evidence
}, "completed");
}
@@ -268,10 +289,19 @@ async function runCaseRunWorker(context: CaseContext) {
const workerContext = { ...context, parsed: { ...context.parsed, runId, runDir }, rest: ["run", caseId] };
await writeRunControl(workerContext, await loadControlRunSkeleton(workerContext, caseId, runId, runDir), { status: "running", stage: "worker-running", startedAt: context.now() });
try {
const payload = await runCaseRun(workerContext);
let payload = await runCaseRun(workerContext);
const run = await readRunFromDir(runDir);
await writeRunControl(workerContext, run, { status: "completed", stage: "completed", completedAt: context.now(), exitCode: 0, resultPath: path.join(runDir, "result.json") });
const completed = await writeRunControl(workerContext, run, { status: "completed", stage: "completed", completedAt: context.now(), exitCode: 0, resultPath: path.join(runDir, "result.json") });
await writeJson(path.join(runDir, "result.json"), payload);
if (workerContext.parsed.noCaseRepoRecord !== true) {
const evidence = await readJsonIfExists(path.join(runDir, "evidence.json"));
if (evidence) {
const archive = await archiveCaseRunArtifacts(workerContext, completed.run, evidence);
payload = { ...payload, collect: collectSummaryFromArchive(completed.run, evidence, archive) };
await writeJson(path.join(runDir, "result.json"), payload);
await archiveCaseRunArtifacts(workerContext, completed.run, evidence);
}
}
return payload;
} catch (error) {
const run = await readRunFromDir(runDir).catch(() => loadControlRunSkeleton(workerContext, caseId, runId, runDir));
@@ -477,17 +507,13 @@ export async function buildCaseRun(context: CaseContext, prepared?: PreparedCase
export async function collectCaseRun(context: CaseContext, prepared?: PreparedCaseRun, knownEvidence?: any) {
const run = prepared ?? await loadRunFromDirOrFail(context);
const evidence = knownEvidence ?? JSON.parse(await readFile(path.join(run.runDir, "evidence.json"), "utf8"));
const files: string[] = [];
if (context.parsed.noCaseRepoRecord !== true) {
const evidenceRel = path.join("runs", run.caseId, run.runId, "evidence.json");
const summaryRel = path.join("runs", run.caseId, run.runId, "summary.md");
await writeJson(path.join(run.caseRepo, evidenceRel), evidence);
await writeFile(path.join(run.caseRepo, summaryRel), renderEvidenceSummary(evidence), "utf8");
files.push(evidenceRel, summaryRel);
}
const summary = {
const archive = context.parsed.noCaseRepoRecord === true ? null : await archiveCaseRunArtifacts(context, run, evidence);
const summary = archive ? collectSummaryFromArchive(run, evidence, archive) : {
caseRepo: run.caseRepo,
caseRepoFiles: files,
caseRepoRunDir: null,
caseRepoFiles: [],
artifactManifestPath: null,
trace: null,
status: evidence.status,
autoEvaluation: false
};
@@ -1114,6 +1140,10 @@ function sha256(value: string) {
return createHash("sha256").update(value).digest("hex");
}
function sha256Buffer(value: Buffer) {
return createHash("sha256").update(value).digest("hex");
}
async function prepareSubjectWorktree(context: CaseContext, input: { caseId: string; runId: string; runDir: string; apiUrl: string; definition: Record<string, unknown>; sourceSpecPath: string }) {
const subject = subjectFromDefinition(input.definition);
const worktreePath = subjectWorktreePath(subject.repoLocalPath, input.runId);
@@ -1337,6 +1367,147 @@ function renderEvidenceSummary(evidence: any) {
return `# HWPOD CaseRun ${evidence.caseId}\n\n- runId: ${evidence.runId}\n- status: ${evidence.status}\n- autoEvaluation: false\n- compileOnly: ${evidence.compileOnly}\n- subjectRepoLocalPath: ${evidence.subject?.repoLocalPath ?? ""}\n- subjectCommitId: ${evidence.subject?.commitId ?? ""}\n- subjectWorktreePath: ${evidence.subject?.worktreePath ?? ""}\n- agentTraceId: ${evidence.agent?.traceId ?? ""}\n- agentSessionId: ${evidence.agent?.sessionId ?? ""}\n- agentTraceCommandCount: ${evidence.agentTrace?.commandCount ?? 0}\n- agentTraceHwpodCommandCount: ${evidence.agentTrace?.hwpodCommandCount ?? 0}\n- agentTraceHwpodBuildCommandCount: ${evidence.agentTrace?.hwpodBuildCommandCount ?? 0}\n- diffPatchPath: ${evidence.agentDiff?.diffPatchPath ?? ""}\n- runnerHwpodSource: ${evidence.hwpod?.source ?? ""}\n- hwpodExitCode: ${evidence.hwpod?.exitCode ?? ""}\n- jobId: ${evidence.keilJob?.jobId ?? ""}\n- downloadSkipped: true\n`;
}
async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRun, evidence: any): Promise<CaseRegistryArchive> {
const runRel = caseRegistryRunRel(run);
const caseRepoRunDir = path.join(run.caseRepo, runRel);
await mkdir(caseRepoRunDir, { recursive: true });
await writeJson(path.join(caseRepoRunDir, "evidence.json"), evidence);
await writeFile(path.join(caseRepoRunDir, "summary.md"), renderEvidenceSummary(evidence), "utf8");
const materialized = new Map<string, string | undefined>([
["evidence.json", path.join(run.runDir, "evidence.json")],
["summary.md", undefined]
]);
const skippedFiles: string[] = [];
for (const entry of caseRunArtifactEntries(run)) {
const rel = artifactRel(entry.rel);
const copied = await copyCaseRunArtifact(entry.source, path.join(caseRepoRunDir, rel));
if (copied) materialized.set(rel, entry.source);
else skippedFiles.push(rel);
}
const files: CaseRegistryArchiveFile[] = [];
for (const [rel, source] of materialized) files.push(await registryArtifactFileRecord(run, caseRepoRunDir, rel, source));
const manifest = caseRunArtifactManifest(context, run, evidence, files, skippedFiles);
const artifactManifestPath = path.join(caseRepoRunDir, "artifact-manifest.json");
await writeJson(artifactManifestPath, manifest);
const caseRepoFiles = [...files.map((file) => artifactRel(path.join(runRel, file.path))), artifactRel(path.join(runRel, "artifact-manifest.json"))];
return { caseRepoRunDir, artifactManifestPath, caseRepoFiles, files, skippedFiles, manifest };
}
function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive: CaseRegistryArchive) {
return {
caseRepo: run.caseRepo,
caseRepoRunDir: archive.caseRepoRunDir,
caseRepoFiles: archive.caseRepoFiles,
artifactManifestPath: archive.artifactManifestPath,
artifactCount: archive.files.length + 1,
skippedFiles: archive.skippedFiles,
trace: archive.manifest.trace ?? null,
status: evidence.status,
autoEvaluation: false
};
}
function caseRunArtifactEntries(run: PreparedCaseRun) {
return [
{ rel: "run.json", source: path.join(run.runDir, "run.json") },
{ rel: "result.json", source: path.join(run.runDir, "result.json") },
{ rel: "agent-prompt.md", source: path.join(run.runDir, "agent-prompt.md") },
{ rel: "agent-diff.patch", source: path.join(run.runDir, "agent-diff.patch") },
{ rel: path.join(".hwlab", "hwpod-spec.yaml"), source: run.specPath },
{ rel: "worker.stdout.log", source: path.join(run.runDir, "worker.stdout.log") },
{ rel: "worker.stderr.log", source: path.join(run.runDir, "worker.stderr.log") }
];
}
async function copyCaseRunArtifact(source: string, destination: string) {
try {
const info = await stat(source);
if (!info.isFile()) return false;
await mkdir(path.dirname(destination), { recursive: true });
await copyFile(source, destination);
return true;
} catch {
return false;
}
}
async function registryArtifactFileRecord(run: PreparedCaseRun, caseRepoRunDir: string, rel: string, source?: string): Promise<CaseRegistryArchiveFile> {
const file = path.join(caseRepoRunDir, rel);
const content = await readFile(file);
return clean({
path: artifactRel(rel),
bytes: content.length,
sha256: sha256Buffer(content),
source: artifactSourceLabel(run, source)
});
}
function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evidence: any, files: CaseRegistryArchiveFile[], skippedFiles: string[]) {
return clean({
contractVersion: "hwpod-case-run-artifact-manifest-v1",
caseId: run.caseId,
runId: run.runId,
archivedAt: context.now(),
sourceRunDir: run.runDir,
caseRepoRunDir: path.join(run.caseRepo, caseRegistryRunRel(run)),
trace: caseRunArtifactTrace(run, evidence),
subject: evidence.subject ?? run.subject,
agent: clean({
providerProfile: evidence.agent?.providerProfile ?? run.agent?.providerProfile,
projectId: evidence.agent?.projectId ?? run.agent?.projectId,
conversationId: evidence.agent?.conversationId ?? run.agent?.conversationId,
sessionId: evidence.agent?.sessionId ?? run.agent?.sessionId,
threadId: evidence.agent?.threadId ?? run.agent?.threadId,
traceId: evidence.agent?.traceId ?? run.agent?.traceId
}),
prompt: clean({ path: "agent-prompt.md", sha256: evidence.agent?.promptSha256 ?? run.agent?.promptSha256 }),
diff: clean({ path: "agent-diff.patch", sha256: evidence.agentDiff?.diffPatchSha256 ?? run.agentDiff?.diffPatchSha256, statusShort: evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort, diffStat: evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat }),
keilJob: evidence.keilJob ?? null,
artifacts: evidence.artifacts ?? [],
decisions: {
autoEvaluation: false,
downloadSkipped: true,
reason: "flow-only run: the manifest records artifacts and trace without auto-grading or gate decisions"
},
files,
skippedFiles
});
}
function caseRunArtifactTrace(run: PreparedCaseRun, evidence: any) {
return clean({
traceId: evidence.agent?.traceId ?? run.agent?.traceId ?? evidence.agentTrace?.traceId ?? run.agentTrace?.traceId,
sessionId: evidence.agent?.sessionId ?? run.agent?.sessionId,
conversationId: evidence.agent?.conversationId ?? run.agent?.conversationId,
threadId: evidence.agent?.threadId ?? run.agent?.threadId,
providerProfile: evidence.agent?.providerProfile ?? run.agent?.providerProfile,
projectId: evidence.agent?.projectId ?? run.agent?.projectId,
status: evidence.agentTrace?.status ?? run.agentTrace?.status,
terminalStatus: evidence.agentTrace?.terminalStatus ?? run.agentTrace?.terminalStatus,
commandCount: evidence.agentTrace?.commandCount ?? run.agentTrace?.commandCount,
hwpodCommandCount: evidence.agentTrace?.hwpodCommandCount ?? run.agentTrace?.hwpodCommandCount,
hwpodBuildCommandCount: evidence.agentTrace?.hwpodBuildCommandCount ?? run.agentTrace?.hwpodBuildCommandCount,
agentRun: compactObject(evidence.agentTrace?.agentRun ?? run.agentTrace?.agentRun ?? null)
});
}
function caseRegistryRunRel(run: PreparedCaseRun) {
return artifactRel(path.join("runs", run.caseId, run.runId));
}
function artifactRel(value: string) {
return value.replace(/\\/gu, "/");
}
function artifactSourceLabel(run: PreparedCaseRun, source?: string) {
if (!source) return undefined;
const relative = path.relative(path.resolve(run.runDir), path.resolve(source));
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) return artifactRel(relative);
return source;
}
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) {
const proc = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);