From a309f0686725f28d3bf12c3449622d5ac5e5840f Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 8 Jun 2026 13:04:29 +0800 Subject: [PATCH] fix: polish caserun aggregate trace markdown --- docs/reference/spec-hwpod-harness.md | 4 +-- tools/hwlab-cli/caserun.test.ts | 16 ++++++--- tools/src/hwlab-cli/trace-renderer.ts | 47 ++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index d80db386..e7c42dfc 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -301,7 +301,7 @@ CaseRun 的 trace 处理必须遵循 [spec-v02-code-agent-trace.md](spec-v02-cod `hwlab-cli case aggregate --run-id --case-repo /root/hwlab-case-registry` 是已完成 CaseRun 的无服务二次整理入口。它只读取 case registry repo 里已有的 `runs///` 产物,不启动新的 CaseRun、不访问硬件、不触发 CI/CD、不追加自动评价或 pass/fail 判定;默认输出 `runs///aggregate.md`,并只把这个 markdown 文件 `git add`、`git commit`、`git push` 回 case registry repo。若 registry 里存在其他并行 dirty 产物,`case aggregate` 不得顺带提交它们。 -`aggregate.md` 是该 run 的主阅读入口,必须按固定顺序聚合运行环境信息、HWPOD 信息、Code Agent 信息、输入 Prompt、低噪声 Trace、final response 和最后 diff。Trace 正文优先使用 `agent-messages.json` 里的共享 Web/CLI renderer rows;普通 message row 直接展示正文,不做折叠,只有工具调用 row 使用 `
/`,且 `` 本身就是折叠按钮,文案写成 `已运行 `。缺少 rows 时才嵌入已有 `agent-trace.md`。聚合文件可以列原始产物索引和 trace/result/inspect 命令作为审计线索,但不能把 `summary.md`、`result.json`、`final-response.md` 或原 trace 文件继续声明为对外主阅读路径。 +`aggregate.md` 是该 run 的主阅读入口,必须按固定顺序聚合运行环境信息、HWPOD 信息、Code Agent 信息、输入 Prompt、低噪声 Trace、final response 和最后 diff。Trace 正文优先使用 `agent-messages.json` 里的共享 Web/CLI renderer rows;普通 message row 直接加粗展示正文,不做折叠,只有工具调用 row 使用 `-
/` 列表项折叠,且 `` 本身就是折叠按钮,文案写成单行截短的 `已运行 `。工具调用展开体必须保留或恢复 `stdout:`、`stderr:`、`exitCode=` 等结构换行,不能把详情挤成一行。缺少 rows 时才嵌入已有 `agent-trace.md`。聚合文件可以列原始产物索引和 trace/result/inspect 命令作为审计线索,但不能把 `summary.md`、`result.json`、`final-response.md` 或原 trace 文件继续声明为对外主阅读路径。 验收已完成 case04 时,使用现有 registry 产物回放,不重新启动 CaseRun: @@ -328,6 +328,6 @@ hwlab-cli case aggregate d601-f103-v2-arm2d-integration \ 7. 长任务验收必须覆盖 `hwlab-cli case run start ` 立即返回,以及 `status/result/logs ` 在 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 是否完成任务。 -10. 已完成 run 的 registry 聚合验收必须通过 `hwlab-cli case aggregate --run-id ` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diff;Trace 中 message 直接展示,工具调用用 `已运行 ` summary 折叠。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。 +10. 已完成 run 的 registry 聚合验收必须通过 `hwlab-cli case aggregate --run-id ` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diff;Trace 中 message 加粗直接展示,工具调用用 `-
` 缩进和单行截短的 `已运行 ` summary 折叠,展开后保留详情换行。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。 11. Windows subject worktree 文本编辑必须能在 CRLF 文件中通过 `workspace.apply-patch` 完成源码修改,返回 before/after SHA 与 diff 摘要,并保留原文件 CRLF;`workspace.apply-patch` 在 context 不匹配时必须返回可诊断 payload,而不是只给 `context not found`。 12. Keil `debug.download` 自动装配必须输出结构化 `cmd.run` argv 步骤;带空格的 `probeName` 在 plan 中必须是单独 argv 元素。`io.uart.read` 必须通过节点本地 `serial-monitor` 读取正在监控的 COM/baud;未监控或工具失败时必须返回包含 `ioProbe`/COM 口/baud/monitor status/启动建议的结构化 blocker details。 diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 3e5b951b..5a3eccf2 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -684,14 +684,16 @@ test("case aggregate writes one registry markdown entry without grading or broad artifacts: [], decisions: { runnerPostAgentCompileCheck: "recorded" } }); + const longTraceCommand = "hwpod workspace cat projects/01_baseline/Middlewares/Arm-2D/Library/include/arm_2d_types.h --spec .hwlab/hwpod-spec.yaml 2>&1 | grep -A 20 \"typedef struct arm_2d_tile_t\" 2>/dev/null || hwpod workspace cat projects/01_baseline/Middlewares/Arm-2D/Library/include/arm_2d_types.h --spec .hwlab/hwpod-spec.yaml 2>&1 | grep"; await writeJsonFile(path.join(registryRunDir, "agent-messages.json"), { renderer: "tools/src/hwlab-cli/trace-renderer:traceDisplayRows", - sourceEventCount: 3, - renderedRowCount: 3, + sourceEventCount: 4, + renderedRowCount: 4, rows: [ { rowId: "row-1", seq: 1, header: "请求接受", body: "CaseRun started" }, { rowId: "row-2", seq: 2, header: "ok commandExecution", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nexitCode=0" }, - { rowId: "row-3", seq: 3, header: "助手最终消息", terminal: true, bodyFormat: "markdown", body: "Build completed" } + { rowId: "row-3", seq: 3, header: "ok commandExecution", body: `${longTraceCommand} stdout: first line\nsecond line exitCode=0` }, + { rowId: "row-4", seq: 4, header: "助手最终消息", terminal: true, bodyFormat: "markdown", body: "Build completed" } ] }); await writeJsonFile(path.join(registryRunDir, "artifact-manifest.json"), { @@ -743,8 +745,12 @@ test("case aggregate writes one registry markdown entry without grading or broad assert.match(aggregate, /## 输入 Prompt/u); assert.match(aggregate, /## 低噪声 Trace/u); assert.doesNotMatch(aggregate, /• 2\. ok commandExecution/u); - assert.match(aggregate, /
\n已运行 hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u); - assert.match(aggregate, /已运行 hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>[\s\S]*```text\nhwpod build --spec \.hwlab\/hwpod-spec\.yaml\nexitCode=0[\s\S]*<\/details>\n\nBuild completed\n\n## Final Response/u); + assert.match(aggregate, /\*\*CaseRun started\*\*/u); + assert.match(aggregate, /-
\n 已运行 hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u); + assert.match(aggregate, / ```text\n hwpod build --spec \.hwlab\/hwpod-spec\.yaml\n exitCode=0[\s\S]* <\/details>/u); + assert.match(aggregate, /已运行 hwpod workspace cat projects\/01_baseline\/Middlewares\/Arm-2D\/Library\/include\/arm_2d_type\.\.\.<\/summary>/u); + assert.match(aggregate, / ```text\n hwpod workspace cat [^\n]+\n stdout:\n first line\n second line\n exitCode=0[\s\S]* <\/details>\n\n\*\*Build completed\*\*\n\n## Final Response/u); + for (const [, summary] of aggregate.matchAll(/([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|grep -A 20/u); assert.doesNotMatch(aggregate, /.*助手最终消息/u); assert.match(aggregate, /## Final Response/u); assert.match(aggregate, /## 最后 Diff/u); diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 36df1c29..e1699538 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -86,21 +86,21 @@ export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string { export function renderTraceRowMarkdown(row: TraceEventRow, index = 1): string { if (!isToolTraceRow(row)) return renderTraceMessageRowMarkdown(row); - const body = renderTraceRowDetailBody(row) || row.header || `trace row ${index}`; + const body = renderTraceToolDetailBody(row) || row.header || `trace row ${index}`; return [ - `
`, - `${escapeHtml(`已运行 ${traceToolSummary(row)}`)}`, + `-
`, + ` ${escapeHtml(`已运行 ${traceToolSummary(row)}`)}`, ``, - traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body), + indentTraceMarkdownBlock(traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body)), ``, - `
` + `
` ].join("\n"); } function renderTraceMessageRowMarkdown(row: TraceEventRow): string { const body = row.body?.trimEnd(); - if (body) return body; - return row.header || `_No readable trace row body._`; + if (body) return traceBoldMarkdown(body); + return traceBoldMarkdown(row.header || `_No readable trace row body._`); } function isToolTraceRow(row: TraceEventRow): boolean { @@ -109,7 +109,7 @@ function isToolTraceRow(row: TraceEventRow): boolean { function traceToolSummary(row: TraceEventRow): string { const firstLine = traceMarkdownPreviewLines(row.body).find((line) => !/^(stdout|stderr|exitCode|rowId|terminal):/iu.test(line.trim())); - return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 320); + return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 90); } function stripTraceToolOutputFromSummary(value: string): string { @@ -483,6 +483,37 @@ function renderTraceRowDetailBody(row: TraceEventRow): string { ].filter(Boolean).join("\n"); } +function renderTraceToolDetailBody(row: TraceEventRow): string { + return normalizeTraceToolDetailBody(renderTraceRowDetailBody(row)); +} + +function normalizeTraceToolDetailBody(value: string): string { + return String(value ?? "") + .replace(/\r\n|\r/gu, "\n") + .replace(/[^\S\n]+(stdout:)[^\S\n]*/giu, "\n$1\n") + .replace(/[^\S\n]+(stderr:)[^\S\n]*/giu, "\n$1\n") + .replace(/[^\S\n]+(exitCode=)/giu, "\n$1") + .replace(/\n{3,}/gu, "\n\n") + .trimEnd(); +} + +function traceBoldMarkdown(value: string): string { + const body = String(value ?? "").trimEnd(); + if (!body) return ""; + return body.split("\n").map((line) => { + if (!line.trim()) return ""; + return `**${escapeMarkdownStrongText(line)}**`; + }).join("\n"); +} + +function escapeMarkdownStrongText(value: string): string { + return escapeHtml(value).replace(/\\/gu, "\\\\").replace(/\*/gu, "\\*"); +} + +function indentTraceMarkdownBlock(value: string): string { + return String(value).split("\n").map((line) => ` ${line}`).join("\n"); +} + function escapeHtml(value: string): string { return String(value).replace(/&/gu, "&").replace(//gu, ">"); }