case-run: archive artifacts to registry
This commit is contained in:
@@ -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 manifest,manifest 只记录文件哈希、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 是否完成任务。
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user