From 1d5c3d422d96049a04869bf606cc0ae9626e866c Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 6 Jun 2026 23:08:19 +0800 Subject: [PATCH] fix: seed caserun hwpod tools --- docs/reference/spec-hwpod-harness.md | 2 + skills/hwpod-cli/SKILL.md | 8 ++- tools/hwlab-cli/caserun.test.ts | 14 +++++ tools/hwpod-harness.test.ts | 34 ++++++++++ tools/src/hwlab-caserun-lib.ts | 28 ++++++++- tools/src/hwpod-harness-lib.ts | 92 ++++++++++++++++++++++++++-- 6 files changed, 170 insertions(+), 8 deletions(-) diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index 1012b47a..ca947784 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -237,6 +237,8 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs --tail 8000 `case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 记录 agent identity、terminal result 里已有的终态字段、trace lookup hint、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build` 和 `cmd.run` Keil 异步 job-status 查询,最后把原始证据和可读归档写入 case registry repo 的 `runs///`。固定归档包括 `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`,以及 `agent-messages.json`、`agent-trace.md`、`agent-transcript.md` 和 `final-response.md`。`agent-trace.md` / `agent-transcript.md` 必须包含 trace lookup hint、renderer 标识和 subject diff 区块;如果归档了完整 rendered rows,其语义必须来自共享 Web/CLI trace renderer,不能由 CaseRun 自行发明。`final-response.md` 在 terminal result 存在权威 finalResponse 时保存正文,不存在时显式记录 `finalResponse=null`、terminal/error rows 和缺失原因。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。 +CaseRun 下发给 AgentRun 的 workspace 必须带 run-local spec 和当前 source tree 中的 HWPOD 工具种子:`tools/hwpod-cli.ts`、`tools/hwpod-ctl.ts`、`tools/hwpod-compiler-cli.ts`、`tools/src/hwpod-harness-lib.ts`、相关合同/endpoint resolver,以及 `.agents/skills/hwpod-cli|hwpod-ctl|hwlab-agent-runtime/SKILL.md`。这不是把 prompt 写成答案,而是保证 Code Agent 看到的 `hwpod` 能力、help、别名和 `cmd.run` 组合语义与当前 `v0.2` 源码一致;不得只发送 `.hwlab/hwpod-spec.yaml` 后依赖运行面预装旧工具或旧 skill 缓存。若 agent 报告标准 `hwpod` 命令缺失或 help 明显过时,优先修 CaseRun workspace seed 或 HWPOD CLI/skill 装配,不新增 hwpod-node 专用 op,也不把业务动作旁路到 shell 脚本。 + CaseRun 的 trace 处理必须遵循 [spec-v02-code-agent-trace.md](spec-v02-code-agent-trace.md) 的组合优先原则:CaseRun 只负责稳定返回和归档 `traceId`、`sessionId`、`conversationId`、`threadId`、AgentRun 执行线索以及 `traceLookup.commands.*`,完整 trace/result/session 读取继续使用已有 `hwlab-cli client agent trace/result/inspect`。CaseRun 不应在自身内部再实现一套 `/v1/agent/chat/trace/{traceId}` 查询器、row renderer、session 反查或 final response 归因逻辑;如果已有 agent CLI 的 trace 可见性不足,先改进 `client agent trace/result/inspect`,再让 CaseRun 输出新的 ID 或 hint。归档中的 `agent-trace.json` 可以是 `lookupOnly=true` 的 identity manifest,表示“用这些 ID 调既有 CLI 查完整 trace”,不能伪装成完整 trace dump。 长时间 CaseRun 必须优先使用 `case run start/status/result/logs` 短连接形态,而不是用 shell/nohup 包装同步 `case run`。`start` 只负责创建 `.state/hwlab-cli/caserun//`、启动后台 worker 并立即返回 `runId`、PID、stdout/stderr 路径和下一条查询命令;`status`、`result`、`logs` 都是短查询。`run.json` 是阶段状态权威,阶段开始时就要刷新 `status`、`stage`、`traceId/sessionId/conversationId/threadId`、AgentRun/trace 轮询信息、stdout/stderr byte count 和下一条轮询命令;长等待期间不得出现无输出、无 trace、无状态文件更新的黑洞窗口。 diff --git a/skills/hwpod-cli/SKILL.md b/skills/hwpod-cli/SKILL.md index c8c4e5d9..96f78595 100644 --- a/skills/hwpod-cli/SKILL.md +++ b/skills/hwpod-cli/SKILL.md @@ -22,12 +22,16 @@ The HWPOD spec is code-agent-side state. The default path is `.hwlab/hwpod-spec. ## Commands - Inspect the HWPOD path: `hwpod inspect --spec .hwlab/hwpod-spec.yaml`. -- Workspace read: `hwpod workspace ls . --spec .hwlab/hwpod-spec.yaml`. -- Build/download/reset: use `hwpod build`, `hwpod download`, and `hwpod reset`; the compiler must translate these to `debug.build`, `debug.download`, and `debug.reset` node ops. +- Workspace read/edit: use `hwpod workspace ls|cat|read|write|replace|insert-after|apply-patch --spec .hwlab/hwpod-spec.yaml`. `read` is an alias for `cat`; `--file` and `--remote-path` are accepted as path aliases. +- Build/download/reset: use `hwpod build`, `hwpod download`, and `hwpod reset`; the compiler must translate these through existing file ops and `cmd.run` where possible. +- Keil async status: after build/download returns a job id, use `hwpod job status --spec .hwlab/hwpod-spec.yaml` instead of shell polling wrappers. +- UART read: use `hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1`; the compiler runs the configured serial-monitor CLI through `cmd.run`. - For early debug bindings, put command bindings in `.hwlab/hwpod-spec.yaml` with `spec.workspace.buildCommand`, `spec.debugProbe.downloadCommand`, or `spec.debugProbe.resetCommand`. ## Boundaries - Do not pass manual API URLs in locked AgentRun runtimes. Let `HWLAB_RUNTIME_API_URL` / `HWLAB_RUNTIME_NAMESPACE` select the current lane. - Do not put full `hwpod-spec` on `hwpod-node`. The node only accepts `hwpod-node-ops`. +- Treat `cmd.run` as the full-capability host execution op. Do not request new hwpod-node ops for build, download, job status, UART, PowerShell/cmd passthrough, or tool maintenance; improve `hwpod-compiler-cli` / `hwpod-cli` combinations first. +- Use workspace file operations for subject text edits. Do not use PowerShell/cmd heredocs for normal source edits unless debugging `cmd.run` itself. - If HWPOD returns a blocker, report and fix that blocker in this path. diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index c8f6bf54..df66678d 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -474,6 +474,20 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit 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.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); diff --git a/tools/hwpod-harness.test.ts b/tools/hwpod-harness.test.ts index dc8421eb..a2d99ce4 100644 --- a/tools/hwpod-harness.test.ts +++ b/tools/hwpod-harness.test.ts @@ -192,6 +192,40 @@ test("hwpod-cli dry-run invokes hwpod-compiler-cli and exposes hwpod-node-ops pl } }); +test("hwpod-cli exposes subcommand help before required args and accepts workspace path aliases", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-cli-help-")); + const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml"); + try { + const workspaceHelp = await runHwpodCli(["workspace", "--help"], { now: () => NOW }); + assert.equal(workspaceHelp.exitCode, 0); + assert.equal(workspaceHelp.payload.action, "hwpod-cli.workspace.help"); + assert.match(JSON.stringify(workspaceHelp.payload.usage), /workspace cat --file /u); + + const writeHelp = await runHwpodCli(["workspace", "write", "--help"], { now: () => NOW }); + assert.equal(writeHelp.exitCode, 0); + assert.equal(writeHelp.payload.action, "hwpod-cli.workspace.help"); + assert.match(JSON.stringify(writeHelp.payload.aliases), /remotePath/u); + + const jobHelp = await runHwpodCli(["job", "status", "--help"], { now: () => NOW }); + assert.equal(jobHelp.exitCode, 0); + assert.equal(jobHelp.payload.action, "hwpod-cli.job.help"); + assert.match(JSON.stringify(jobHelp.payload.usage), /hwpod job status /u); + + await runHwpodCtl(["spec", "init", "--spec", specPath, "--node", "pc-host-1"], { now: () => NOW }); + const catByFile = await runHwpodCli(["workspace", "cat", "--spec", specPath, "--file", "projects/01_baseline/User/main.c", "--dry-run"], { now: () => NOW }); + assert.equal(catByFile.exitCode, 0); + assert.equal(catByFile.payload.plan.ops[0].op, "workspace.cat"); + assert.equal(catByFile.payload.plan.ops[0].args.path, "projects/01_baseline/User/main.c"); + + const writeByRemotePath = await runHwpodCli(["workspace", "write", "--spec", specPath, "--remote-path", "projects/01_baseline/User/main.c", "--content", "int main(void){}", "--dry-run"], { now: () => NOW }); + assert.equal(writeByRemotePath.exitCode, 0); + assert.equal(writeByRemotePath.payload.plan.ops[0].op, "workspace.write"); + assert.equal(writeByRemotePath.payload.plan.ops[0].args.path, "projects/01_baseline/User/main.c"); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("hwpod-cli submits compiled node ops to hwlab-api when not dry-run", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-cli-submit-")); const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml"); diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 12ead8b2..d3afdfd1 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -3,6 +3,7 @@ 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"; @@ -19,6 +20,18 @@ 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" }, + { source: "skills/hwpod-cli/SKILL.md", target: ".agents/skills/hwpod-cli/SKILL.md" }, + { source: "skills/hwpod-ctl/SKILL.md", target: ".agents/skills/hwpod-ctl/SKILL.md" }, + { source: "skills/hwlab-agent-runtime/SKILL.md", target: ".agents/skills/hwlab-agent-runtime/SKILL.md" } +] as const; type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; @@ -911,7 +924,17 @@ function agentTraceIdentityArtifact(run: PreparedCaseRun, agent: AgentRunStage) async function agentWorkspaceFilesForRun(run: PreparedCaseRun): Promise { const content = await readFile(run.specPath, "utf8"); - return [{ path: ".hwlab/hwpod-spec.yaml", content, encoding: "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 }) { @@ -1016,6 +1039,7 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg "只能修改 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。", "若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。", "不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。", @@ -1033,7 +1057,7 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg `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. Treat that file as the authority for workspace/toolchain/debug/IO bindings. Do not create or overwrite it from this prompt.`, + `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.`, ``, `## Task`, agentTask.prompt, diff --git a/tools/src/hwpod-harness-lib.ts b/tools/src/hwpod-harness-lib.ts index f8cdf28f..67477a60 100644 --- a/tools/src/hwpod-harness-lib.ts +++ b/tools/src/hwpod-harness-lib.ts @@ -101,6 +101,7 @@ export async function runHwpodCli(argv: string[], options: { env?: EnvLike; fetc const parsed = parseOptions(argv); const command = parsed._[0] || "help"; if (["help", "--help", "-h"].includes(command)) return result(0, cliHelp(), now); + if (parsed.help === true || parsed.h === true) return result(0, hwpodCliCommandHelp(command, parsed), now); if (command === "closeout") return result(0, closeout(parsed), now); const specPath = specPathFrom(parsed); @@ -223,6 +224,85 @@ function cliHelp() { }); } +function hwpodCliCommandHelp(command: string, parsed: ParsedArgs) { + if (command === "workspace") return hwpodWorkspaceHelp(text(parsed._[1])); + if (command === "job") return hwpodJobHelp(text(parsed._[1])); + if (command === "build") return hwpodBuildHelp(); + if (command === "download") return hwpodDownloadHelp(); + if (command === "uart") return hwpodUartHelp(text(parsed._[1])); + if (command === "cmd") return hwpodCmdHelp(); + return cliHelp(); +} + +function hwpodWorkspaceHelp(subcommand = "") { + return ok("hwpod-cli.workspace.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + command: "workspace", + subcommand: subcommand || null, + usage: [ + "hwpod workspace ls [path] --spec .hwlab/hwpod-spec.yaml", + "hwpod workspace cat --spec .hwlab/hwpod-spec.yaml", + "hwpod workspace read --spec .hwlab/hwpod-spec.yaml", + "hwpod workspace cat --file --spec .hwlab/hwpod-spec.yaml", + "hwpod workspace cat --remote-path --spec .hwlab/hwpod-spec.yaml", + "hwpod workspace write --path --content --expected-sha ", + "hwpod workspace replace --path --find --replace --expected-sha ", + "hwpod workspace insert-after --path --anchor --line --expected-sha ", + "cat patch.txt | hwpod workspace apply-patch --spec .hwlab/hwpod-spec.yaml --reason " + ], + aliases: { + read: "workspace.cat", + file: "--path", + remotePath: "--path" + }, + boundary: "Workspace file operations are the standard editing path; do not use shell quoting for subject text edits unless debugging cmd.run itself." + }); +} + +function hwpodJobHelp(subcommand = "") { + return ok("hwpod-cli.job.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + command: "job", + subcommand: subcommand || null, + usage: ["hwpod job status --spec .hwlab/hwpod-spec.yaml"], + boundary: "Job status compiles to cmd.run against the configured Keil CLI; it is not a separate hwpod-node op." + }); +} + +function hwpodBuildHelp() { + return ok("hwpod-cli.build.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + usage: ["hwpod build --spec .hwlab/hwpod-spec.yaml", "hwpod build --spec .hwlab/hwpod-spec.yaml --dry-run"], + boundary: "Build is a short HWPOD CLI invocation that normally starts an async Keil job through cmd.run." + }); +} + +function hwpodDownloadHelp() { + return ok("hwpod-cli.download.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + usage: ["hwpod download --spec .hwlab/hwpod-spec.yaml", "hwpod download --spec .hwlab/hwpod-spec.yaml --dry-run"], + boundary: "Download compiles to a Keil flash cmd.run plan; do not add a download-specific hwpod-node op." + }); +} + +function hwpodUartHelp(subcommand = "") { + return ok("hwpod-cli.uart.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + command: "uart", + subcommand: subcommand || null, + usage: ["hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1"], + boundary: "UART read is compiled to cmd.run against the configured serial-monitor CLI." + }); +} + +function hwpodCmdHelp() { + return ok("hwpod-cli.cmd.help", { + contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, + usage: ["hwpod cmd [...argv] --spec .hwlab/hwpod-spec.yaml"], + boundary: "cmd.run is the full-capability host execution op; add compiler/CLI combinations before considering new hwpod-node ops." + }); +} + async function initSpec(parsed: ParsedArgs, now: () => string) { const specPath = specPathFrom(parsed); const document = defaultSpecDocument(parsed, now); @@ -339,12 +419,12 @@ function commandToIntent(parsed: ParsedArgs, stdinText?: string) { if (command === "workspace") { const subcommand = parsed._[1] || "ls"; if (subcommand === "ls") return { intent: "workspace.ls", args: { path: text(parsed.path ?? parsed._[2] ?? ".") } }; - if (subcommand === "cat" || subcommand === "read") return { intent: "workspace.cat", args: { path: requiredText(parsed.path ?? parsed._[2], "path") } }; + if (subcommand === "cat" || subcommand === "read") return { intent: "workspace.cat", args: { path: requiredWorkspacePath(parsed, 2) } }; if (subcommand === "rg") return { intent: "workspace.rg", args: { pattern: requiredText(parsed.pattern ?? parsed._[2], "pattern"), path: text(parsed.path ?? parsed._[3] ?? "."), ignoreCase: parsed.ignoreCase === true } }; if (subcommand === "apply-patch") return { intent: "workspace.apply-patch", args: { patch: patchText(parsed.patch ?? parsed.patchText ?? stdinText), patchBase64: text(parsed.patchBase64), reason: text(parsed.reason) } }; - if (subcommand === "write") return { intent: "workspace.write", args: clean({ path: requiredText(parsed.path ?? parsed._[2], "path"), content: patchText(parsed.content ?? parsed.contentText ?? stdinText), contentBase64: text(parsed.contentBase64), expectedSha: text(parsed.expectedSha), lineEnding: text(parsed.lineEnding), finalNewline: parsed.finalNewline === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; - if (subcommand === "replace") return { intent: "workspace.replace", args: cleanTextEditArgs({ path: requiredText(parsed.path ?? parsed._[2], "path"), find: requiredRawString(parsed.find ?? parsed._[3], "find"), replace: rawString(parsed.replace ?? parsed.replacement ?? parsed._[4]), expectedSha: text(parsed.expectedSha), all: parsed.all === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; - if (subcommand === "insert-after") return { intent: "workspace.insert-after", args: cleanTextEditArgs({ path: requiredText(parsed.path ?? parsed._[2], "path"), anchor: requiredRawString(parsed.anchor ?? parsed._[3], "anchor"), line: requiredRawString(parsed.line ?? parsed.insert ?? parsed.text ?? parsed._[4], "line"), expectedSha: text(parsed.expectedSha), allowMultiple: parsed.allowMultiple === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; + if (subcommand === "write") return { intent: "workspace.write", args: clean({ path: requiredWorkspacePath(parsed, 2), content: patchText(parsed.content ?? parsed.contentText ?? stdinText), contentBase64: text(parsed.contentBase64), expectedSha: text(parsed.expectedSha), lineEnding: text(parsed.lineEnding), finalNewline: parsed.finalNewline === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; + if (subcommand === "replace") return { intent: "workspace.replace", args: cleanTextEditArgs({ path: requiredWorkspacePath(parsed, 2), find: requiredRawString(parsed.find ?? parsed._[3], "find"), replace: rawString(parsed.replace ?? parsed.replacement ?? parsed._[4]), expectedSha: text(parsed.expectedSha), all: parsed.all === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; + if (subcommand === "insert-after") return { intent: "workspace.insert-after", args: cleanTextEditArgs({ path: requiredWorkspacePath(parsed, 2), anchor: requiredRawString(parsed.anchor ?? parsed._[3], "anchor"), line: requiredRawString(parsed.line ?? parsed.insert ?? parsed.text ?? parsed._[4], "line"), expectedSha: text(parsed.expectedSha), allowMultiple: parsed.allowMultiple === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) }; throw cliError("unsupported_workspace_command", `unsupported workspace command: ${subcommand}`); } if (command === "build") return { intent: "debug.build", args: clean({ target: text(parsed.target), command: text(parsed.command ?? parsed.commandLine), wait: parsed.wait === true, timeoutMs: numberValue(parsed.timeoutMs), reason: text(parsed.reason) }) }; @@ -966,6 +1046,10 @@ function specPathFrom(parsed: ParsedArgs) { return text(parsed.spec ?? parsed.specPath) || DEFAULT_HWPOD_SPEC_PATH; } +function requiredWorkspacePath(parsed: ParsedArgs, positionalIndex: number) { + return requiredText(parsed.path ?? parsed.file ?? parsed.remotePath ?? parsed.remote ?? parsed._[positionalIndex], "path"); +} + function result(exitCode: number, payload: any, now: () => string) { return { exitCode, payload: { ...payload, observedAt: now() } }; }