切换 AgentRun 资源装配到 gitbundle

This commit is contained in:
Codex Agent
2026-06-08 11:25:54 +08:00
parent c52ba0dac2
commit 9d76230bc3
17 changed files with 152 additions and 239 deletions
+19 -33
View File
@@ -37,19 +37,20 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state",
const runLocalSpec = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-test", ".hwlab", "hwpod-spec.yaml"), "utf8");
assert.match(runLocalSpec, /node-d601-f103-v2/u);
assert.match(runLocalSpec, /path: "F:\\\\Work\\\\HWLAB-CASE-F103\\\\\.worktree\\\\caserun-run-test"/u);
assert.equal(requests.length, 1);
assert.equal(requests[0].body.ops.length, 4);
assert.equal(requests[0].body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH);
assert.deepEqual(requests[0].body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]);
assert.deepEqual(requests[0].body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]);
assert.deepEqual(requests[0].body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]);
assert.equal(requests[0].body.ops[3].opId, "op_04_keil_sidecars");
assert.equal(requests[0].body.ops[3].args.command, "node");
const sidecarInput = JSON.parse(requests[0].body.ops[3].args.argv[2]);
const setupRequest = requests.find((item) => item.body?.ops?.some((op: any) => op.opId === "op_02_subject_worktree_add"));
assert.ok(setupRequest);
assert.equal(setupRequest.body.ops.length, 4);
assert.equal(setupRequest.body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH);
assert.deepEqual(setupRequest.body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]);
assert.deepEqual(setupRequest.body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]);
assert.deepEqual(setupRequest.body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]);
assert.equal(setupRequest.body.ops[3].opId, "op_04_keil_sidecars");
assert.equal(setupRequest.body.ops[3].args.command, "node");
const sidecarInput = JSON.parse(setupRequest.body.ops[3].args.argv[2]);
assert.equal(sidecarInput.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(sidecarInput.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(requests[0].body.ops[3].args.commandBinding.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(requests[0].body.ops[3].args.commandBinding.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(setupRequest.body.ops[3].args.commandBinding.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(setupRequest.body.ops[3].args.commandBinding.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103");
assert.equal(result.payload.subject.setup.keilSidecars.copied, 1);
} finally {
await rm(root, { recursive: true, force: true });
@@ -462,41 +463,26 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
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);
assert.match(prompt, /Runtime Assembly/u);
assert.match(prompt, /kind=gitbundle/u);
assert.match(prompt, /verificationMode: compile-only build check/u);
assert.doesNotMatch(prompt, /compile-only subject /u);
assert.doesNotMatch(prompt, /Write exactly this YAML/u);
assert.doesNotMatch(prompt, /```yaml/u);
assert.doesNotMatch(prompt, /apiVersion: hwlab\.pikastech\.com/u);
assert.doesNotMatch(prompt, /path: "F:\\\\Work\\\\HWLAB-CASE-F103/u);
assert.match(prompt, /CaseRun has already placed the run-local spec/u);
assert.match(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.doesNotMatch(prompt, /CaseRun has already placed the run-local spec/u);
assert.doesNotMatch(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u);
assert.match(prompt, /hwpod job status <jobId> --spec \.hwlab\/hwpod-spec\.yaml/u);
assert.match(prompt, /hwpod job status <jobId>/u);
assert.match(prompt, / shell sleep\/&&\/timeout\/watch\/head\/pipe/u);
assert.match(prompt, /separate short `hwpod job status <jobId> --spec \.hwlab\/hwpod-spec\.yaml` commands/u);
assert.match(prompt, /separate short `hwpod job status <jobId>` commands/u);
const diff = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-diff.patch"), "utf8");
assert.match(diff, /printf\("hello"\)/u);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/sessions"), true);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat"), true);
const agentChatRequest = requests.find((item) => item.url === "http://web.test/v1/agent/chat");
assert.equal(agentChatRequest.body.workspaceFiles[0].path, ".hwlab/hwpod-spec.yaml");
assert.equal(agentChatRequest.body.workspaceFiles[0].encoding, "utf8");
assert.match(agentChatRequest.body.workspaceFiles[0].content, /HWLAB-CASE-F103.*caserun-run-agent-flow/u);
const workspaceFilePaths = agentChatRequest.body.workspaceFiles.map((item: any) => item.path);
assert.equal(workspaceFilePaths.includes("tools/hwpod-cli.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/hwpod-ctl.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/hwpod-compiler-cli.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/hwpod-harness-lib.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/hwpod-node-ops-contract.ts"), true);
assert.equal(workspaceFilePaths.includes("tools/src/runtime-endpoint-resolver.ts"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwpod-cli/SKILL.md"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwpod-ctl/SKILL.md"), true);
assert.equal(workspaceFilePaths.includes(".agents/skills/hwlab-agent-runtime/SKILL.md"), true);
const seededHwpodLib = agentChatRequest.body.workspaceFiles.find((item: any) => item.path === "tools/src/hwpod-harness-lib.ts");
assert.match(seededHwpodLib.content, /hwpod-cli\.job\.help/u);
const seededHwpodSkill = agentChatRequest.body.workspaceFiles.find((item: any) => item.path === ".agents/skills/hwpod-cli/SKILL.md");
assert.match(seededHwpodSkill.content, /cmd\.run/u);
assert.equal(Object.hasOwn(agentChatRequest.body, "workspaceFiles"), false);
assert.doesNotMatch(agentChatRequest.body.message, /path: "F:\\Work\\HWLAB-CASE-F103\\\.worktree\\caserun-run-agent-flow"/u);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/result/" + result.payload.agent.traceId), true);
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/trace/" + result.payload.agent.traceId), false);
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwlab-code-agent-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-compiler-cli.ts" "$@"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/hwpod-ctl.ts" "$@"
+8 -40
View File
@@ -3,7 +3,6 @@ import { createHash, randomUUID } from "node:crypto";
import { closeSync, openSync } from "node:fs";
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
@@ -20,22 +19,12 @@ const HWLAB_API_KEY_PREFIX = "hwl_live_";
const STANDARD_CASE_REPO_PATH = "/root/hwlab-case-registry";
const REMOVED_CASE_REPO_PATH = "/root/hwpod-cases";
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows";
const SOURCE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const AGENT_WORKSPACE_SEED_FILES = [
{ source: "tools/hwpod-cli.ts", target: "tools/hwpod-cli.ts" },
{ source: "tools/hwpod-ctl.ts", target: "tools/hwpod-ctl.ts" },
{ source: "tools/hwpod-compiler-cli.ts", target: "tools/hwpod-compiler-cli.ts" },
{ source: "tools/src/hwpod-harness-lib.ts", target: "tools/src/hwpod-harness-lib.ts" },
{ source: "tools/src/hwpod-node-ops-contract.ts", target: "tools/src/hwpod-node-ops-contract.ts" },
{ source: "tools/src/runtime-endpoint-resolver.ts", target: "tools/src/runtime-endpoint-resolver.ts" },
] as const;
type EnvLike = Record<string, string | undefined>;
type ParsedArgs = Record<string, unknown> & { _: string[] };
type FetchLike = typeof fetch;
type RunProcessLike = (command: string[], cwd: string, env: Record<string, string | undefined>) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>;
type StartProcessLike = (input: { command: string; args: string[]; cwd: string; env: Record<string, string | undefined>; stdoutPath: string; stderrPath: string }) => Promise<{ pid: number }> | { pid: number };
type AgentWorkspaceFile = { path: string; content: string; encoding: "utf8" };
type CaseContext = {
parsed: ParsedArgs;
@@ -720,7 +709,6 @@ async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
}
const promptPath = path.join(run.runDir, "agent-prompt.md");
const prompt = await renderAgentTaskPrompt(run, run.agentTask);
const workspaceFiles = await agentWorkspaceFilesForRun(run);
await writeFile(promptPath, prompt, "utf8");
const promptSha256 = sha256(prompt);
const baseUrl = webUrlFrom(context, run.definition);
@@ -738,7 +726,7 @@ async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
const threadId = session.threadId;
const traceId = text(context.parsed.traceId) || `trc_case_${slug(run.caseId)}_${randomUUID().replace(/-/gu, "")}`;
run = await updateRun(context, run, { stage: "agent-submitting", agent: agentFailureStage({ stageStatus: "session_created", baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, promptPath, promptSha256, sessionResponse: compactObject(session.body) }) });
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt, workspaceFiles });
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt });
if (!accepted.ok) {
const agent = agentFailureStage({ stageStatus: "submit_failed", baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, promptPath, promptSha256, accepted: accepted.body, error: accepted.error, sessionResponse: session.body });
const nextRun = await updateRun(context, run, { agent });
@@ -869,7 +857,7 @@ async function createAgentSession(context: CaseContext, input: { baseUrl: string
};
}
async function submitAgentTask(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId: string; threadId: string; traceId: string; prompt: string; workspaceFiles?: AgentWorkspaceFile[] }) {
async function submitAgentTask(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId: string; threadId: string; traceId: string; prompt: string }) {
return caseFetchJson(context, `${input.baseUrl}/v1/agent/chat`, {
method: "POST",
headers: { ...caseAgentHeaders(context), "x-trace-id": input.traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" },
@@ -881,7 +869,6 @@ async function submitAgentTask(context: CaseContext, input: { baseUrl: string; p
traceId: input.traceId,
providerProfile: input.providerProfile,
shortConnection: true,
workspaceFiles: input.workspaceFiles,
projectId: input.projectId,
updatedByClient: "hwlab-cli.case-run"
}))
@@ -938,21 +925,6 @@ function agentTraceIdentityArtifact(run: PreparedCaseRun, agent: AgentRunStage)
});
}
async function agentWorkspaceFilesForRun(run: PreparedCaseRun): Promise<AgentWorkspaceFile[]> {
const content = await readFile(run.specPath, "utf8");
const files: AgentWorkspaceFile[] = [{ path: ".hwlab/hwpod-spec.yaml", content, encoding: "utf8" }];
for (const entry of AGENT_WORKSPACE_SEED_FILES) {
const sourcePath = path.join(SOURCE_ROOT, entry.source);
try {
files.push({ path: entry.target, content: await readFile(sourcePath, "utf8"), encoding: "utf8" });
} catch (error) {
if (error?.code === "ENOENT") throw cliError("caserun_agent_workspace_seed_missing", "CaseRun agent workspace seed file is missing", { source: entry.source, sourcePath });
throw error;
}
}
return files;
}
async function pollAgentResult(context: CaseContext, input: { baseUrl: string; traceId: string; acceptedBody: any; resultUrl?: string; run?: PreparedCaseRun; agent?: AgentRunStage }) {
const requestedTimeoutMs = effectiveAgentTimeoutMs(context, input.run?.agentTask);
const timeoutMs = Math.max(Math.min(requestedTimeoutMs, MAX_AGENT_TIMEOUT_MS), 1000);
@@ -1085,9 +1057,8 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
...agentTask.constraints,
"只能修改 isolated subject worktree,不得修改 case registry repo。",
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
"CaseRun 已在 AgentRun workspace 预装 .hwlab/hwpod-spec.yaml;必须使用这个 run-local HWPOD spec,不要从 prompt 重建 spec。",
"CaseRun 已随本次 run 注入当前 v0.2 的 hwpod/hwpod-ctl/hwpod-compiler 工具文件和 HWPOD skill;若标准 hwpod 命令能力缺失,报告 workspace tool seed 问题,不要改走旁路。",
"如果 .hwlab/hwpod-spec.yaml 缺失或内容明显不是本次 case,请报告 CaseRun workspace setup 错误,不要自行编造或迁移 spec。",
"AgentRun 通过 kind=gitbundle 装配当前 v0.2 的 tools/ 与 .agents/skills;若标准 hwpod 命令能力缺失,报告 gitbundle runtime assembly 问题,不要改走旁路。",
"不要依赖 workspaceFiles、seed files、hostPath skill 目录或 ConfigMap 作为工具/skill 注入 fallback。",
"若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。",
"不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。",
"hwpod build/download 是长任务短连接入口;不要再用 shell sleep/&&/timeout/watch/head/pipe 或 shell loop 包住它们。记录返回 JSON 里的 jobId/job_id,再用独立的 hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml 短命令做有限轮询。"
@@ -1100,11 +1071,10 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
`subjectCommitId: ${run.subject.commitId}`,
`subjectWorktreePath: ${run.subject.worktreePath}`,
`runLocalHwpodSpecWorkspacePath: .hwlab/hwpod-spec.yaml`,
`verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`,
``,
`## Run-local HWPOD spec`,
`CaseRun has already placed the run-local spec at \`.hwlab/hwpod-spec.yaml\` in this AgentRun workspace before your turn starts, and has seeded the current v0.2 hwpod/hwpod-ctl/hwpod-compiler tool files and HWPOD skills. Treat the run-local spec as the authority for workspace/toolchain/debug/IO bindings. Do not create or overwrite it from this prompt.`,
`## Runtime Assembly`,
`AgentRun materializes HWLAB runtime assets from ResourceBundleRef kind=gitbundle: repo subpath \`tools\` is copied to workspace \`tools\`, and repo subpath \`skills\` is copied to workspace \`.agents/skills\`. CaseRun no longer sends ad hoc workspaceFiles or seed-file payloads.`,
``,
`## Task`,
agentTask.prompt,
@@ -1113,11 +1083,9 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
...constraints.map((item) => `- ${item}`),
``,
`## Flow`,
`- Confirm \`.hwlab/hwpod-spec.yaml\` exists. If it is missing, report a CaseRun workspace setup error instead of reconstructing it.`,
`- Run \`hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml\`.`,
`- Run \`hwpod inspect --spec .hwlab/hwpod-spec.yaml\`.`,
`- Confirm standard \`hwpod\`, \`hwpod-ctl\` and \`hwpod-compiler\` commands are available from the gitbundle tools directory.`,
`- Follow the case task using standard hwpod/hwpod-ctl commands. Run build/download/UART steps only when the case explicitly asks for them, and report returned JSON/job/artifact/serial summaries.`,
`- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with separate short \`hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml\` commands a bounded number of times. Do not wrap status polling with shell sleep, &&, timeout, watch, head, pipes, or shell loops.`,
`- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with separate short \`hwpod job status <jobId>\` commands a bounded number of times. Do not wrap status polling with shell sleep, &&, timeout, watch, head, pipes, or shell loops.`,
`- CaseRun will inspect git diff under subjectWorktreePath after your turn completes and may run a runner post-check compile as separate evidence.`,
`- CaseRun records trace/session/conversation, agent commandExecution, workspace diff and Keil build evidence without auto-grading them.`
].join("\n");
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec bun "$(dirname "$0")/unidesk-ssh.mjs" "$@"