切换 AgentRun 资源装配到 gitbundle
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$(dirname "$0")/hwlab-code-agent-cli.ts" "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$(dirname "$0")/hwpod-cli.ts" "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$(dirname "$0")/hwpod-compiler-cli.ts" "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$(dirname "$0")/hwpod-ctl.ts" "$@"
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$(dirname "$0")/unidesk-ssh.mjs" "$@"
|
||||
Reference in New Issue
Block a user