From 34ee9472857ab43c77d8d302101bfa0580c27867 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 8 Jun 2026 13:27:16 +0800 Subject: [PATCH] feat: add caserun prompt preview --- tools/hwlab-cli/caserun.test.ts | 60 ++++++++++++++++++--- tools/src/hwlab-caserun-lib.ts | 94 +++++++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 30 deletions(-) diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 5a3eccf2..e773a3af 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -103,6 +103,49 @@ test("case prepare uses the new registry by default and refuses the removed hwpo } }); +test("case prompt previews the real assembled prompt in Chinese without starting a run", async () => { + const root = await mkdtempCaseRoot(); + const caseRepo = path.join(root, "hwlab-case-registry"); + const cwd = path.join(root, "hwlab"); + const caseDir = path.join(caseRepo, "cases", "d601-f103-v2-prompt-preview"); + try { + await mkdir(path.join(caseRepo, ".git"), { recursive: true }); + await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); + await mkdir(caseDir, { recursive: true }); + await mkdir(cwd, { recursive: true }); + await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ + contractVersion: "hwpod-case-v1", + caseId: "d601-f103-v2-prompt-preview", + title: "D601-F103-V2 中文 Prompt 预览", + hwpodSpec: "hwpod-spec.yaml", + subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" }, + agentTask: { workspace: "subjectWorktree", prompt: "请在隔离主体工作区中读取 main.c,并只回报当前文件中与串口初始化相关的函数名。", constraints: ["不要修改源码。"] } + }, null, 2), "utf8"); + await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); + + const result = await runHwlabCli(["case", "prompt", "d601-f103-v2-prompt-preview", "--case-repo", caseRepo, "--run-id", "run-prompt-preview"], { cwd, now: () => "2026-06-08T00:00:00.000Z" }); + + assert.equal(result.exitCode, 0); + assert.equal(result.payload.action, "case.prompt"); + assert.equal(result.payload.previewOnly, true); + assert.equal(result.payload.serviceRuntime, false); + assert.match(result.payload.prompt, /# HWPOD CaseRun 代码代理任务/u); + assert.match(result.payload.prompt, /## 运行时装配/u); + assert.match(result.payload.prompt, /## HWPOD 运行时/u); + assert.match(result.payload.prompt, /## 任务/u); + assert.match(result.payload.prompt, /## 约束/u); + assert.match(result.payload.prompt, /## 执行流程/u); + assert.match(result.payload.prompt, /思维过程和输出消息一律使用中文/u); + assert.match(result.payload.prompt, /验证模式: 仅执行编译构建检查/u); + assert.match(result.payload.prompt, /请在隔离主体工作区中读取 main\.c/u); + assert.match(result.payload.prompt, /hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path/u); + assert.doesNotMatch(result.payload.prompt, /Runtime Assembly|Standard smoke sequence|verificationMode: compile-only build check|## Task|## Constraints|## Flow/u); + await assert.rejects(stat(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-prompt-preview"))); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("case run extracts Keil async job ids from hwpod-node output", () => { const jobId = extractKeilJobId({ body: { results: [{ stdout: JSON.stringify({ ok: true, job_id: "20260605_203835_798515c0" }) }] } }); assert.equal(jobId, "20260605_203835_798515c0"); @@ -284,7 +327,7 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit title: "D601-F103-V2 Keil compile-only CaseRun", hwpodSpec: "hwpod-spec.yaml", subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" }, - agentTask: { workspace: "subjectWorktree", prompt: "Make a tiny source change through HWPOD.", timeoutMs: 234000, pollIntervalMs: 10 }, + agentTask: { workspace: "subjectWorktree", prompt: "请通过 HWPOD 做一个很小的源码修改。", timeoutMs: 234000, pollIntervalMs: 10 }, runtime: { apiUrl: "http://api.test", webUrl: "http://web.test" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); @@ -462,10 +505,13 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.doesNotMatch(summaryMarkdown, /downloadSkipped/u); 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, /Runtime Assembly/u); + assert.match(prompt, /不做自动评分或门禁判断/u); + assert.match(prompt, /运行时装配/u); assert.match(prompt, /kind=gitbundle/u); - assert.match(prompt, /verificationMode: compile-only build check/u); + assert.match(prompt, /思维过程和输出消息一律使用中文/u); + assert.match(prompt, /验证模式: 仅执行编译构建检查/u); + assert.match(prompt, /请通过 HWPOD 做一个很小的源码修改/u); + assert.doesNotMatch(prompt, /Runtime Assembly|verificationMode: compile-only build check|Standard smoke sequence|## Task|## Constraints|## Flow|without auto-grading/u); assert.doesNotMatch(prompt, /compile-only 阶段不要修改 subject 源码/u); assert.doesNotMatch(prompt, /Write exactly this YAML/u); assert.doesNotMatch(prompt, /```yaml/u); @@ -477,10 +523,10 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.match(prompt, /hwpodWorkspaceArgs: --hwpod-id d601-f103-v2 --workspace-path 'F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-agent-flow'/u); assert.match(prompt, /hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path/u); assert.match(prompt, /不要创建、复制或修补本地 spec 文件/u); - assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u); + assert.match(prompt, /标准 hwpod\/hwpod-ctl 命令/u); assert.match(prompt, /hwpod job status /u); - assert.match(prompt, /不要再用 shell sleep\/&&\/timeout\/watch\/head\/pipe/u); - assert.match(prompt, /separate short `hwpod job status --hwpod-id d601-f103-v2 --workspace-path/u); + assert.match(prompt, /不要用 shell sleep、&&、timeout、watch、head、pipe/u); + assert.match(prompt, /独立短命令 `hwpod job status --hwpod-id d601-f103-v2 --workspace-path/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); diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index d0b3a537..4d411a37 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -206,6 +206,7 @@ type ArchivedAgentTraceSnapshot = { export async function caseCommand(context: CaseContext) { const subcommand = context.rest[0] || "help"; if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp(); + if (subcommand === "prompt") return promptCaseRun(context); if (subcommand === "aggregate") return aggregateCaseRun(context); if (subcommand === "prepare") return prepareCaseRun(context); if (subcommand === "build") return buildCaseRun(context); @@ -237,6 +238,7 @@ export function caseHelp() { commitId: "fixed subject repo commit id; no GitHub clone/fetch fallback" }, commands: [ + `case prompt CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-id RUN_ID]`, `case prepare CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-id RUN_ID]`, `case build CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-dir DIR]`, `case collect CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} --run-dir DIR`, @@ -248,6 +250,7 @@ export function caseHelp() { `case run logs RUN_ID [--tail 8000]` ], notes: [ + "case prompt 是无服务预览入口,只按真实 CaseRun 组装路径渲染输入 prompt,不准备 worktree、不提交 agent、不访问硬件。", "case build remains a compile-only HWPOD smoke stage.", "case run prepares the subject worktree, submits a prompt to HWLAB Code Agent, records agent provenance and workspace diff, then runs compile validation.", "case aggregate is a service-free post-processing command: it reads an existing case registry run and writes one aggregate markdown entry without pass/fail grading.", @@ -257,6 +260,50 @@ export function caseHelp() { }); } +export async function promptCaseRun(context: CaseContext) { + const caseId = requiredText(context.parsed.caseId ?? context.rest[1], "caseId"); + const caseRepo = await resolveCaseRepo(context); + const loaded = await loadCaseDefinition(caseRepo, caseId); + const runId = runIdFrom(context, caseId); + const runDir = path.resolve(context.cwd, text(context.parsed.runDir) || path.join(stateRoot(context), runId)); + const subject = subjectFromDefinition(loaded.definition); + const worktreePath = subjectWorktreePath(subject.repoLocalPath, runId); + const agentTask = agentTaskFromDefinition(loaded.definition, caseId); + const now = context.now(); + const run: PreparedCaseRun = { + caseId, + runId, + runDir, + caseRepo, + caseDir: loaded.caseDir, + caseFile: loaded.caseFile, + sourceSpecPath: loaded.specPath, + specPath: loaded.specPath, + apiUrl: apiUrlFrom(context, loaded.definition), + compileOnly: true, + createdAt: now, + updatedAt: now, + definition: loaded.definition, + subject: { ...subject, worktreePath, workspacePath: worktreePath, setup: { previewOnly: true } }, + agentTask + }; + const prompt = await renderAgentTaskPrompt(run, agentTask); + return ok("case.prompt", { + caseId, + runId, + runDir, + caseRepo, + sourceSpecPath: loaded.specPath, + compileOnly: true, + serviceRuntime: false, + previewOnly: true, + promptSha256: sha256(prompt), + agentTask: agentTaskSummary(agentTask), + subject: { repoLocalPath: subject.repoLocalPath, commitId: subject.commitId, worktreePath }, + prompt + }); +} + export async function aggregateCaseRun(context: CaseContext) { const caseRepo = await resolveCaseRepo(context); const aggregate = await writeCaseAggregateFromRegistry(context, caseRepo, { sync: true }); @@ -1080,6 +1127,7 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg const hwpodId = text(hwpodDocument.metadata.name) || text(hwpodDocument.metadata.uid) || run.caseId; const hwpodWorkspaceArgs = `--hwpod-id ${hwpodId} --workspace-path ${promptShellArg(run.subject.worktreePath)}`; const constraints = [ + "思维过程和输出消息一律使用中文", ...agentTask.constraints, "只能修改 isolated subject worktree,不得修改 case registry repo。", "不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。", @@ -1092,41 +1140,41 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg `hwpod build/download 是长任务短连接入口;不要再用 shell sleep/&&/timeout/watch/head/pipe 或 shell loop 包住它们。记录返回 JSON 里的 jobId/job_id,再用独立的 hwpod job status ${hwpodWorkspaceArgs} 短命令做有限轮询。` ].filter(Boolean); return [ - `# HWPOD CaseRun Code Agent Task`, + `# HWPOD CaseRun 代码代理任务`, ``, - `caseId: ${run.caseId}`, - `runId: ${run.runId}`, - `subjectRepoLocalPath: ${run.subject.repoLocalPath}`, - `subjectCommitId: ${run.subject.commitId}`, - `subjectWorktreePath: ${run.subject.worktreePath}`, + `案例ID: ${run.caseId}`, + `运行ID: ${run.runId}`, + `主体仓库本地路径: ${run.subject.repoLocalPath}`, + `主体提交ID: ${run.subject.commitId}`, + `主体隔离工作区路径: ${run.subject.worktreePath}`, `hwpodId: ${hwpodId}`, `hwpodWorkspaceArgs: ${hwpodWorkspaceArgs}`, - `verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`, + `验证模式: 仅执行编译构建检查;除非案例明确要求,否则不下载、不做运行态冒烟验证。`, ``, - `## 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.`, + `## 运行时装配`, + `AgentRun 通过 ResourceBundleRef kind=gitbundle 装配 HWLAB 运行时资源:仓库子路径 \`tools\` 会复制到工作区 \`tools\`,仓库子路径 \`skills\` 会复制到工作区 \`.agents/skills\`。CaseRun 不再发送临时 workspaceFiles 或 seed-file 载荷。`, ``, - `## HWPOD Runtime`, - `Use HWPOD by id through the runtime service. Do not require a runner-local \`.hwlab/hwpod-spec.yaml\`.`, - `Use these arguments on every hwpod/hwpod-ctl command: \`${hwpodWorkspaceArgs}\`.`, - `Standard smoke sequence for this task:`, + `## HWPOD 运行时`, + `通过运行时服务按 ID 使用 HWPOD,不要求 runner 本地存在 \`.hwlab/hwpod-spec.yaml\`。`, + `每个 hwpod/hwpod-ctl 命令都必须携带这些参数:\`${hwpodWorkspaceArgs}\`。`, + `本任务的标准冒烟步骤:`, `- \`hwpod-ctl spec validate ${hwpodWorkspaceArgs}\``, `- \`hwpod inspect ${hwpodWorkspaceArgs}\``, - `- workspace reads/searches/edits: \`hwpod workspace ... ${hwpodWorkspaceArgs}\``, - `- compile check: \`hwpod build ${hwpodWorkspaceArgs}\``, + `- 工作区读取、搜索和编辑:\`hwpod workspace ... ${hwpodWorkspaceArgs}\``, + `- 编译检查:\`hwpod build ${hwpodWorkspaceArgs}\``, ``, - `## Task`, + `## 任务`, agentTask.prompt, ``, - `## Constraints`, + `## 约束`, ...constraints.map((item) => `- ${item}`), ``, - `## Flow`, - `- 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 with \`${hwpodWorkspaceArgs}\`. 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 ${hwpodWorkspaceArgs}\` 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.` + `## 执行流程`, + `- 先确认标准 \`hwpod\`、\`hwpod-ctl\` 和 \`hwpod-compiler\` 命令可从 gitbundle 工具目录调用。`, + `- 使用标准 hwpod/hwpod-ctl 命令并携带 \`${hwpodWorkspaceArgs}\` 完成案例任务。只有案例明确要求时才运行 build/download/UART 步骤,并回报返回的 JSON、job、artifact 或串口摘要。`, + `- 对 hwpod build/download,保持 HWPOD 命令本身不被包装,让它返回异步 JSON;随后用独立短命令 \`hwpod job status ${hwpodWorkspaceArgs}\` 对返回的 job id 做有限轮询。不要用 shell sleep、&&、timeout、watch、head、pipe 或 shell loop 包裹状态轮询。`, + `- 你的回合结束后,CaseRun 会检查 subjectWorktreePath 下的 git diff,并可能把 runner 后置编译作为单独证据记录。`, + `- CaseRun 只记录 trace、session、conversation、agent 命令执行、workspace diff 和 Keil 构建证据,不做自动评分或门禁判断。` ].join("\n"); }