fix: resolve hwpod by id for caserun agents
This commit is contained in:
@@ -2,23 +2,25 @@
|
|||||||
|
|
||||||
> **hwpod-node 运维操作见 `hwpod-ops` skill**(`~/.agents/skills/hwpod-ops/SKILL.md`),包含 G14 serve 模式、D601 Windows connect 模式启停、cloud-api 注册状态检查、源码同步、日志路径和故障排查。
|
> **hwpod-node 运维操作见 `hwpod-ops` skill**(`~/.agents/skills/hwpod-ops/SKILL.md`),包含 G14 serve 模式、D601 Windows connect 模式启停、cloud-api 注册状态检查、源码同步、日志路径和故障排查。
|
||||||
|
|
||||||
本文是 HWLAB `v0.2` 的 HWPOD Harness 长期规格。概念体系和实施跟踪见 [pikasTech/HWLAB#897](https://github.com/pikasTech/HWLAB/issues/897)。当前实现只打通单线程核心业务闭环,不把鉴权、安全、并发、计量、复杂调度或 Evidence 体系作为前置条件。
|
本文是 HWLAB `v0.2` 的 HWPOD Harness 长期规格。概念体系和实施跟踪见 [pikasTech/HWLAB#897](https://github.com/pikasTech/HWLAB/issues/897);HWPOD 集中服务式管理和 `hwpod-id` source-of-truth 收敛见 [pikasTech/HWLAB#1043](https://github.com/pikasTech/HWLAB/issues/1043)。当前实现只打通单线程核心业务闭环,不把鉴权、安全、并发、计量、复杂调度或 Evidence 体系作为前置条件。
|
||||||
|
|
||||||
本规格不保留旧设备执行路径作为迁移对照;新任务只按 HWPOD 单通道实现和验收。
|
本规格不保留旧设备执行路径作为迁移对照;新任务只按 HWPOD 单通道实现和验收。
|
||||||
|
|
||||||
## 快速迭代阶段
|
## 默认运行通道
|
||||||
|
|
||||||
快速迭代阶段把业务翻译权放在 Code Agent workspace 内:
|
当前 Code Agent / CaseRun runner 的默认 HWPOD 合同是 `hwpod-id` 加本次任务的 workspace binding,不再要求 runner workspace 预装 `.hwlab/hwpod-spec.yaml`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Code Agent workspace
|
Code Agent workspace
|
||||||
.hwlab/hwpod-spec.yaml
|
|
||||||
hwpod-cli
|
hwpod-cli
|
||||||
hwpod-ctl
|
hwpod-ctl
|
||||||
hwpod-compiler-cli
|
hwpod-compiler-cli
|
||||||
|
task context: hwpodId + workspacePath
|
||||||
|
|
||||||
hwpod-cli / hwpod-ctl
|
hwpod-cli / hwpod-ctl
|
||||||
-> hwpod-compiler-cli
|
-> runtime HWPOD registry (/v1/hwpod/specs?hwpodId=...)
|
||||||
|
-> HWPOD document + run workspace override
|
||||||
|
-> hwpod-compiler-cli / in-process compiler
|
||||||
-> hwpod-node-ops plan
|
-> hwpod-node-ops plan
|
||||||
-> hwlab-api
|
-> hwlab-api
|
||||||
-> hwpod-node
|
-> hwpod-node
|
||||||
@@ -26,19 +28,22 @@ hwpod-cli / hwpod-ctl
|
|||||||
|
|
||||||
核心边界:
|
核心边界:
|
||||||
|
|
||||||
- `hwpod-spec` 存放在 Code Agent workspace 内,是当前 hwpod 的本地声明式定义。
|
- `hwpod-id` 是 WebUI、CLI、CaseRun、Code Agent 和 runner 的默认运行时引用。
|
||||||
|
- HWPOD 定义由 runtime service 解析;runner-local YAML 只作为显式 debug/import/export,不作为默认合同。
|
||||||
- `hwpod-cli` 是用户和 Code Agent 执行研发动作的入口。
|
- `hwpod-cli` 是用户和 Code Agent 执行研发动作的入口。
|
||||||
- `hwpod-ctl` 是和 `hwpod-cli` 平级的管理入口,用于初始化、修改、检查 `hwpod-spec`,以及 smoke/node 状态类操作。
|
- `hwpod-ctl` 是和 `hwpod-cli` 平级的管理入口,用于校验 `hwpod-id` 解析结果、检查四要素/node binding,以及 smoke/node 状态类操作。
|
||||||
- `hwpod-compiler-cli` 是 workspace-local 编译器,把高层 intent 和 `hwpod-spec` 编译为 `hwpod-node-ops`。
|
- `hwpod-compiler-cli` / in-process compiler 把高层 intent 和 runtime-resolved HWPOD document 编译为 `hwpod-node-ops`。
|
||||||
- `hwlab-api` 只接收 `hwpod-node-ops` plan,负责转发和返回结果,不承担业务翻译。
|
- `hwlab-api` 只接收 `hwpod-node-ops` plan,负责转发和返回结果,不承担业务翻译。
|
||||||
- `hwpod-node` 是靠近硬件的薄执行节点,只维护少量稳定 op handler,不保存完整 `hwpod-spec`,也不解释高层 hwpod intent。
|
- `hwpod-node` 是靠近硬件的薄执行节点,只维护少量稳定 op handler,不保存完整 `hwpod-spec`,也不解释高层 hwpod intent。
|
||||||
|
|
||||||
## 云端平台阶段
|
## 云端平台阶段
|
||||||
|
|
||||||
快速闭环跑通后,再逐步迁移为云端平台能力:
|
完整服务式 registry/source-of-truth 仍按 #1043 收敛;当前已具备最小 discovery/resolve 能力:
|
||||||
|
|
||||||
- 将 workspace-local `hwpod-spec` 注册或同步到云端,形成云端 hwpod source of truth。
|
- `GET /v1/hwpod/specs` 可列出已知 HWPOD,并可按 `hwpodId` 查询用于 CLI/runner resolve。
|
||||||
- 将 `hwpod-ctl spec` 从本地文件管理迁移为云端 hwpod/spec 管理,同时保留本地导入、导出和调试能力。
|
- `hwpod-ctl spec validate --hwpod-id <id> --workspace-path <run-worktree>` 校验 runtime-resolved document 和四要素。
|
||||||
|
- `hwpod-cli ... --hwpod-id <id> --workspace-path <run-worktree>` 生成并提交 node-ops plan。
|
||||||
|
- 将本地 YAML 文件管理降级为显式导入、导出和调试能力,不再作为 AgentRun runner 默认合同。
|
||||||
- 将 `hwpod-compiler-cli` 服务化为云端 `hwpod-compiler`,供 Web、Code Agent、batch harness 和 CI/HIL gate 共用。
|
- 将 `hwpod-compiler-cli` 服务化为云端 `hwpod-compiler`,供 Web、Code Agent、batch harness 和 CI/HIL gate 共用。
|
||||||
- 将 `hwlab-api` 从薄转发面扩展为 hwpod run 管理面,承载 run 状态、计划版本、结果归档和后续 Evidence。
|
- 将 `hwlab-api` 从薄转发面扩展为 hwpod run 管理面,承载 run 状态、计划版本、结果归档和后续 Evidence。
|
||||||
- 只在多个任务反复需要同一类基础动作时扩展 `hwpod-node-ops`,避免把业务变化下沉到 node。
|
- 只在多个任务反复需要同一类基础动作时扩展 `hwpod-node-ops`,避免把业务变化下沉到 node。
|
||||||
@@ -48,10 +53,10 @@ hwpod-cli / hwpod-ctl
|
|||||||
| 概念 | 定义 | 当前阶段含义 |
|
| 概念 | 定义 | 当前阶段含义 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `hwpod` | 硬件研发执行逻辑实体 | 由 target device、workspace、debug probe、io probe 四要素组成 |
|
| `hwpod` | 硬件研发执行逻辑实体 | 由 target device、workspace、debug probe、io probe 四要素组成 |
|
||||||
| `hwpod-spec` | hwpod 的声明式定义 | 先存放在 Code Agent workspace 内,后续迁移到云端维护 |
|
| `hwpod-spec` | hwpod 的声明式定义 | 由 runtime service 解析;本地 YAML 仅用于显式 debug/import/export |
|
||||||
| `hwpod-cli` | 用户和 Code Agent 的任务入口 | 发起 inspect、workspace、build、download、reset、UART、JSON-RPC 和 closeout |
|
| `hwpod-cli` | 用户和 Code Agent 的任务入口 | 发起 inspect、workspace、build、download、reset、UART、JSON-RPC 和 closeout |
|
||||||
| `hwpod-ctl` | workspace 内的管理入口 | 初始化/检查/修改 spec、绑定 node、执行 smoke 和临时维护动作 |
|
| `hwpod-ctl` | HWPOD 管理入口 | 检查 `hwpod-id`、四要素、node binding、执行 smoke 和临时维护动作 |
|
||||||
| `hwpod-compiler-cli` | workspace-local HWPOD 编译器 | 把高层 intent + spec 编译为 `hwpod-node-ops` plan |
|
| `hwpod-compiler-cli` | HWPOD 编译器 | 把高层 intent + resolved document 编译为 `hwpod-node-ops` plan |
|
||||||
| `hwpod-compiler` | 云端化后的编译服务 | 由 `hwpod-compiler-cli` 成熟后服务化而来 |
|
| `hwpod-compiler` | 云端化后的编译服务 | 由 `hwpod-compiler-cli` 成熟后服务化而来 |
|
||||||
| `hwpod-node-ops` | node 最小操作协议 | `hwlab-api` 和 `hwpod-node` 之间的稳定执行契约 |
|
| `hwpod-node-ops` | node 最小操作协议 | `hwlab-api` 和 `hwpod-node` 之间的稳定执行契约 |
|
||||||
| `hwpod-node` | 靠近硬件的薄执行节点 | PC host、AI 网关、调试器边缘设备或实验室控制盒上的 executor |
|
| `hwpod-node` | 靠近硬件的薄执行节点 | PC host、AI 网关、调试器边缘设备或实验室控制盒上的 executor |
|
||||||
@@ -66,9 +71,11 @@ hwpod-cli / hwpod-ctl
|
|||||||
|
|
||||||
`nodeBinding` 是 `hwpod-spec` 的部署字段,不属于 hwpod 四个本体要素。
|
`nodeBinding` 是 `hwpod-spec` 的部署字段,不属于 hwpod 四个本体要素。
|
||||||
|
|
||||||
## hwpod-spec
|
## HWPOD identity and document
|
||||||
|
|
||||||
快速迭代阶段的默认 spec 路径是 `.hwlab/hwpod-spec.yaml`。CLI 也可以通过 `--spec <path>` 显式指定。第一版 spec 保持轻量:
|
默认入口通过 `--hwpod-id <id>` 解析 HWPOD document。`--workspace-path <run-worktree>` 是 CaseRun/单步调试绑定本次 subject worktree 的运行时 override。`--spec <path>` 只保留为显式 debug/import/export path,不允许作为 Code Agent runner 缺少 service resolve 时的 fallback。
|
||||||
|
|
||||||
|
第一版 HWPOD document 保持轻量:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: hwlab.dev/v0alpha1
|
apiVersion: hwlab.dev/v0alpha1
|
||||||
@@ -126,9 +133,6 @@ Keil MDK 装配规则:当 `spec.workspace.toolchain` 为 `keil-mdk`、`keil`
|
|||||||
| `workspace.cat` | 读文件 |
|
| `workspace.cat` | 读文件 |
|
||||||
| `workspace.rg` | 递归搜索文件内容,标准别名还有 `grep` / `search` |
|
| `workspace.rg` | 递归搜索文件内容,标准别名还有 `grep` / `search` |
|
||||||
| `workspace.apply-patch` | 应用 Codex apply_patch envelope 源码补丁 |
|
| `workspace.apply-patch` | 应用 Codex apply_patch envelope 源码补丁 |
|
||||||
| `workspace.write` | 带 `expectedSha`、换行策略和 diff 摘要的整文件写入 |
|
|
||||||
| `workspace.replace` | 带 `expectedSha`、唯一匹配检查和 diff 摘要的精确文本替换 |
|
|
||||||
| `workspace.insert-after` | 按精确 anchor 插入文本,避免 PowerShell/cmd quoting |
|
|
||||||
| `cmd.run` | 通用 host 命令透传,用于组合 Keil、serial-monitor、job status、下载、复位和临时维护动作 |
|
| `cmd.run` | 通用 host 命令透传,用于组合 Keil、serial-monitor、job status、下载、复位和临时维护动作 |
|
||||||
|
|
||||||
`workspace.rg` 是查找符号、结构体、函数签名和错误文本的标准入口。它在 node 侧直接做受限递归文本搜索,不依赖目标 host 预装 `rg`,并支持 `--context`、`--before-context`、`--after-context`、`--max-matches`、`--max-files`、`--max-bytes-per-file` 和 `--max-line-bytes`。输出会保留结构化 `matches`、`scannedFiles`、`truncated` 和 `limits`,用于在不爆炸输出的前提下追踪上下文。需要精准读整文件时才用 `workspace.cat`;查找 ARM-2D 头文件、函数原型或 Demo 入口时,优先用 `workspace.rg` / `workspace.search`,不要再把 `cat` 管道给 shell `grep`。
|
`workspace.rg` 是查找符号、结构体、函数签名和错误文本的标准入口。它在 node 侧直接做受限递归文本搜索,不依赖目标 host 预装 `rg`,并支持 `--context`、`--before-context`、`--after-context`、`--max-matches`、`--max-files`、`--max-bytes-per-file` 和 `--max-line-bytes`。输出会保留结构化 `matches`、`scannedFiles`、`truncated` 和 `limits`,用于在不爆炸输出的前提下追踪上下文。需要精准读整文件时才用 `workspace.cat`;查找 ARM-2D 头文件、函数原型或 Demo 入口时,优先用 `workspace.rg` / `workspace.search`,不要再把 `cat` 管道给 shell `grep`。
|
||||||
@@ -137,7 +141,7 @@ Keil MDK 装配规则:当 `spec.workspace.toolchain` 为 `keil-mdk`、`keil`
|
|||||||
|
|
||||||
`hwpod-node` 是 HWPOD 的唯一受控节点执行器。第一阶段的业务扩展优先落在 `hwpod-compiler-cli` / `hwpod-cli`,通过已有文件读写 op 和 `cmd.run` 组合 Keil、serial-monitor 等成熟工具;不要因为某个 case 需要 build、job status、download、UART 或临时维护动作就新增专用 node op。`cmd.run` 的命令解析、PATH 稳定性和跨平台差异必须在 `hwpod-node` 本体内处理;当 Windows service、后台进程或 D601 host 的环境变量与交互 shell 不一致时,不得用 gateway shell、手工 PowerShell、预先手工创建 worktree 或其他旁路代替 `hwpod-node-ops -> hwpod-node`。D601/Windows 实地调试应使用 UniDesk SSH 透传 `D601:win` 观察进程、PATH、工具安装位置和日志,但修复必须回到 `hwpod-node` 源码、配置或启动入口,并用 `/v1/hwpod-node-ops` 原链路复测。只有 `cmd.run` 自身的执行语义、环境继承或可见性不满足真实 host 执行时,才修改 `hwpod-node`;否则优先改 compiler/CLI。
|
`hwpod-node` 是 HWPOD 的唯一受控节点执行器。第一阶段的业务扩展优先落在 `hwpod-compiler-cli` / `hwpod-cli`,通过已有文件读写 op 和 `cmd.run` 组合 Keil、serial-monitor 等成熟工具;不要因为某个 case 需要 build、job status、download、UART 或临时维护动作就新增专用 node op。`cmd.run` 的命令解析、PATH 稳定性和跨平台差异必须在 `hwpod-node` 本体内处理;当 Windows service、后台进程或 D601 host 的环境变量与交互 shell 不一致时,不得用 gateway shell、手工 PowerShell、预先手工创建 worktree 或其他旁路代替 `hwpod-node-ops -> hwpod-node`。D601/Windows 实地调试应使用 UniDesk SSH 透传 `D601:win` 观察进程、PATH、工具安装位置和日志,但修复必须回到 `hwpod-node` 源码、配置或启动入口,并用 `/v1/hwpod-node-ops` 原链路复测。只有 `cmd.run` 自身的执行语义、环境继承或可见性不满足真实 host 执行时,才修改 `hwpod-node`;否则优先改 compiler/CLI。
|
||||||
|
|
||||||
Windows subject worktree 文本修改优先使用 `workspace.apply-patch`、`workspace.replace`、`workspace.insert-after` 或 `workspace.write`,不要把 `cmd.run` 的 PowerShell/cmd quoting 当成标准编辑路径。文本编辑结果必须返回编辑前后 SHA、文件字节数、换行类型、diff 摘要和 dry-run 状态;`apply-patch` 匹配失败时必须返回 normalized preview、候选行号、CRLF/LF 统计、文件 SHA/bytes 和 hwpod-node implementation version,便于判断是上下文不匹配、CRLF 问题还是远端实现版本问题。`workspace.apply-patch` 应保留原文件换行风格,尤其不能把 CRLF subject 文件静默改写成 LF。
|
Windows subject worktree 文本修改优先使用 `workspace.apply-patch`,不要把 `cmd.run` 的 PowerShell/cmd quoting 当成标准编辑路径,也不要把旧 `workspace.write` / `workspace.replace` / `workspace.insert-after` 当成新 case 的推荐入口。文本编辑结果必须返回编辑前后 SHA、文件字节数、换行类型、diff 摘要和 dry-run 状态;`apply-patch` 匹配失败时必须返回 normalized preview、候选行号、CRLF/LF 统计、文件 SHA/bytes 和 hwpod-node implementation version,便于判断是上下文不匹配、CRLF 问题还是远端实现版本问题。`workspace.apply-patch` 应保留原文件换行风格,尤其不能把 CRLF subject 文件静默改写成 LF。
|
||||||
|
|
||||||
UART read 默认由 compiler 把 `hwpod-cli uart read` 编译为已有 `cmd.run` 内的一段短序列,调用节点本地 `serial-monitor` CLI 的 `monitor start` 和 `fetch --session-only`。两步必须在同一个 `cmd.run` 内顺序执行:`monitor start` 的 JSON 若返回 `success:false`,该 `cmd.run` 必须以非 0 退出并停止,不得继续 fetch 旧会话数据。默认绑定路径是 `C:\Users\liang\.agents\skills\serial-monitor`,也可通过 `spec.tooling.serialMonitorDir`、`spec.ioProbe.serialMonitorDir` 或 CLI 参数覆盖;`spec.ioProbe.uart.port` 和 `spec.ioProbe.uart.baudrate` 是物理串口和波特率权威。串口采集失败时必须把 serial-monitor 的 stdout/stderr、exit code 和 command argv 原样留在 `cmd.run` result 中;不得静默伪造空读数,也不得要求 Agent 改走手写 PowerShell/串口脚本旁路。
|
UART read 默认由 compiler 把 `hwpod-cli uart read` 编译为已有 `cmd.run` 内的一段短序列,调用节点本地 `serial-monitor` CLI 的 `monitor start` 和 `fetch --session-only`。两步必须在同一个 `cmd.run` 内顺序执行:`monitor start` 的 JSON 若返回 `success:false`,该 `cmd.run` 必须以非 0 退出并停止,不得继续 fetch 旧会话数据。默认绑定路径是 `C:\Users\liang\.agents\skills\serial-monitor`,也可通过 `spec.tooling.serialMonitorDir`、`spec.ioProbe.serialMonitorDir` 或 CLI 参数覆盖;`spec.ioProbe.uart.port` 和 `spec.ioProbe.uart.baudrate` 是物理串口和波特率权威。串口采集失败时必须把 serial-monitor 的 stdout/stderr、exit code 和 command argv 原样留在 `cmd.run` result 中;不得静默伪造空读数,也不得要求 Agent 改走手写 PowerShell/串口脚本旁路。
|
||||||
|
|
||||||
@@ -177,21 +181,19 @@ type HwpodNodeOpResult = {
|
|||||||
快速阶段的源码入口使用 Bun 直接运行 TypeScript 文件;runner 或 skill 可以把这些入口暴露为短命令:
|
快速阶段的源码入口使用 Bun 直接运行 TypeScript 文件;runner 或 skill 可以把这些入口暴露为短命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun tools/hwpod-ctl.ts spec init --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-ctl.ts spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-ctl.ts spec validate --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-compiler-cli.ts compile --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --intent workspace.ls --args '{"path":"."}'
|
||||||
bun tools/hwpod-compiler-cli.ts compile --spec .hwlab/hwpod-spec.yaml --intent workspace.ls --args '{"path":"."}'
|
bun tools/hwpod-cli.ts inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run
|
||||||
bun tools/hwpod-cli.ts inspect --spec .hwlab/hwpod-spec.yaml --dry-run
|
bun tools/hwpod-cli.ts workspace read projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts workspace read --spec .hwlab/hwpod-spec.yaml projects/01_baseline/User/main.c
|
bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --spec .hwlab/hwpod-spec.yaml
|
cat patch.txt | bun tools/hwpod-cli.ts workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason "edit subject workspace through hwpod-node"
|
||||||
bun tools/hwpod-cli.ts workspace replace --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c --find "old text" --replace "new text" --expected-sha <sha>
|
bun tools/hwpod-cli.ts download --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts workspace insert-after --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c --anchor "while (1)" --line " /* marker */"
|
bun tools/hwpod-cli.ts job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts download --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --port uart1
|
||||||
bun tools/hwpod-cli.ts job status <jobId> --spec .hwlab/hwpod-spec.yaml
|
|
||||||
bun tools/hwpod-cli.ts uart read --spec .hwlab/hwpod-spec.yaml --port uart1
|
|
||||||
bun tools/hwpod-node.ts serve --host 127.0.0.1 --port 19678
|
bun tools/hwpod-node.ts serve --host 127.0.0.1 --port 19678
|
||||||
```
|
```
|
||||||
|
|
||||||
`hwpod-cli` 的正常任务路径是:读取 spec、调用 compiler、把 node-ops plan 交给 `hwlab-api`,最后输出 closeout/result。`hwpod-ctl` 的管理路径可以在本地直接修改 spec,也可以生成 smoke plan 交给 `hwlab-api` 验证 node 链路。
|
`hwpod-cli` 的正常任务路径是:按 `hwpod-id` 从 runtime service resolve HWPOD document、应用 workspace override、编译 node-ops plan、交给 `hwlab-api`,最后输出 closeout/result。`hwpod-ctl` 的管理路径用于校验 resolved document 和 node 链路;本地 YAML 修改只用于显式 debug/import/export。
|
||||||
|
|
||||||
## HWPOD CLI 调试入口分层
|
## HWPOD CLI 调试入口分层
|
||||||
|
|
||||||
@@ -199,7 +201,7 @@ HWPOD 调试固定保留两种入口,二者必须调用同一套 `hwpod-cli` /
|
|||||||
|
|
||||||
### 人工单步直调 hwpod-cli
|
### 人工单步直调 hwpod-cli
|
||||||
|
|
||||||
人工或维护 agent 定位 CaseRun 卡点时,优先在 G14 v0.2 workspace 直接调用 `hwpod-cli` 的单步命令。调用前必须加载 v0.2 CLI API key 和 runtime endpoint,并显式选择 run-local spec 或 case registry spec;spec 是 DAPLink、Keil target、COM 口、波特率和 subject worktree 的唯一输入权威。
|
人工或维护 agent 定位 CaseRun 卡点时,优先在 G14 v0.2 workspace 直接调用 `hwpod-cli` 的单步命令。调用前必须加载 v0.2 CLI API key 和 runtime endpoint,并显式传入 `--hwpod-id` 和本次 subject worktree 的 `--workspace-path`;resolved HWPOD document 是 DAPLink、Keil target、COM 口和波特率的输入权威,workspace path 是本次运行的覆盖参数。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
. /root/.config/hwlab-v02/master-server-admin-api-key.env
|
. /root/.config/hwlab-v02/master-server-admin-api-key.env
|
||||||
@@ -207,34 +209,34 @@ export HWLAB_RUNTIME_API_URL=http://74.48.78.17:19667
|
|||||||
export HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666
|
export HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666
|
||||||
export HWLAB_RUNTIME_ENDPOINT_LOCKED=1
|
export HWLAB_RUNTIME_ENDPOINT_LOCKED=1
|
||||||
|
|
||||||
bun tools/hwpod-ctl.ts spec validate --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-ctl.ts spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts inspect --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts workspace cat --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c
|
bun tools/hwpod-cli.ts workspace cat projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts build --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts build --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts download --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts download --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts job status <jobId> --spec .hwlab/hwpod-spec.yaml
|
bun tools/hwpod-cli.ts job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
bun tools/hwpod-cli.ts uart read --spec .hwlab/hwpod-spec.yaml --port uart1 --max-bytes 4096
|
bun tools/hwpod-cli.ts uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --port uart1 --max-bytes 4096
|
||||||
```
|
```
|
||||||
|
|
||||||
直调入口只用于单步定位和原链路复测:`build` / `download` 返回 Keil async job accepted 只能证明命令已投递,不能等同于编译或下载成功;必须继续用独立短命令 `hwpod-cli job status <jobId>` 轮询终态,不要把轮询包进 `sleep &&`、`timeout`、`watch`、`head`、pipe 或 shell loop。解释结果时结合 `uart read`、Keil result 或目标串口日志。串口被人工工具占用时,应先释放 COM 口或读取已有 serial-monitor 数据解释现象,不得把 COM 口占用误判成 DAPLink、目标板或 API 不可用。
|
直调入口只用于单步定位和原链路复测:`build` / `download` 返回 Keil async job accepted 只能证明命令已投递,不能等同于编译或下载成功;必须继续用独立短命令 `hwpod-cli job status <jobId>` 轮询终态,不要把轮询包进 `sleep &&`、`timeout`、`watch`、`head`、pipe 或 shell loop。解释结果时结合 `uart read`、Keil result 或目标串口日志。串口被人工工具占用时,应先释放 COM 口或读取已有 serial-monitor 数据解释现象,不得把 COM 口占用误判成 DAPLink、目标板或 API 不可用。
|
||||||
|
|
||||||
### CaseRun 让 code agent 调 hwpod-cli
|
### CaseRun 让 code agent 调 hwpod-cli
|
||||||
|
|
||||||
CaseRun 验证 code agent 能力时,runner 的职责是布置舞台,而不是代替 agent 完成源码修改或硬件动作。标准流程是:CaseRun 从 case registry 读取 `case.json` 和 `hwpod-spec.yaml`,创建 subject 隔离 worktree,生成 run-local `.hwlab/hwpod-spec.yaml` 并把 `spec.workspace.path` 改写到该 worktree,再把当前 HWPOD CLI/skill 文件随 AgentRun workspace 一起提供给 code agent。
|
CaseRun 验证 code agent 能力时,runner 的职责是布置舞台,而不是代替 agent 完成源码修改或硬件动作。标准流程是:CaseRun 从 case registry 读取 `case.json` 和 HWPOD 引用,创建 subject 隔离 worktree,把 `hwpodId` 与 `hwpodWorkspaceArgs` 写入 agent prompt/context,再把当前 HWPOD CLI/skill 文件随 AgentRun gitbundle workspace 一起提供给 code agent。runner 不再要求 `.hwlab/hwpod-spec.yaml` 进入 AgentRun workspace。
|
||||||
|
|
||||||
code agent 必须在自己的 AgentRun workspace 内调用同一套入口,例如:
|
code agent 必须在自己的 AgentRun workspace 内调用同一套入口,例如:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml
|
hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod inspect --spec .hwlab/hwpod-spec.yaml
|
hwpod inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod workspace cat --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c
|
hwpod workspace cat projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --spec .hwlab/hwpod-spec.yaml
|
hwpod workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod workspace replace --spec .hwlab/hwpod-spec.yaml --path projects/01_baseline/User/main.c --find <old> --replace <new> --expected-sha <sha>
|
cat patch.txt | hwpod workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason "edit subject workspace through hwpod-node"
|
||||||
hwpod build --spec .hwlab/hwpod-spec.yaml
|
hwpod build --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod download --spec .hwlab/hwpod-spec.yaml
|
hwpod download --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml
|
hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>
|
||||||
hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1 --max-bytes 4096
|
hwpod uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --port uart1 --max-bytes 4096
|
||||||
```
|
```
|
||||||
|
|
||||||
对 Code Agent 而言,Keil/HWPOD 长任务同样遵循 cli-spec 短连接组合:`build` 或 `download` 只负责返回 async job id,`job status` 必须作为独立短命令少量轮询;若仍处于 running,应报告 job id、status、diagnostics 和当前判断,不要用长 shell 等待把 agent 子阶段拖成黑盒。
|
对 Code Agent 而言,Keil/HWPOD 长任务同样遵循 cli-spec 短连接组合:`build` 或 `download` 只负责返回 async job id,`job status` 必须作为独立短命令少量轮询;若仍处于 running,应报告 job id、status、diagnostics 和当前判断,不要用长 shell 等待把 agent 子阶段拖成黑盒。
|
||||||
@@ -243,7 +245,7 @@ CaseRun prompt 只能描述任务、边界、允许修改的文件、必须尝
|
|||||||
|
|
||||||
## CaseRun 无服务阶段
|
## CaseRun 无服务阶段
|
||||||
|
|
||||||
第一阶段 CaseRun 不引入常驻调度服务,由 `hwlab-cli case` 组合 case registry repo、本地 `.state`、`hwpod-cli`、`hwpod-compiler-cli`、`hwlab-api` 和在线 `hwpod-node` 完成单次运行。标准 case registry repo 是 `pikasTech/hwlab-case-registry`,本地默认 checkout 路径是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 只作为迁移前来源。case registry repo 提供 `cases/<caseId>/case.json` 与同目录 `hwpod-spec.yaml`;每次运行会先在 subject repo 本地 checkout 下创建隔离 Git worktree,再把 spec 复制到 `.state/hwlab-cli/caserun/<runId>/.hwlab/hwpod-spec.yaml` 并把 `spec.workspace.path` 改写到该隔离 worktree,形成 run-local spec。
|
第一阶段 CaseRun 不引入常驻调度服务,由 `hwlab-cli case` 组合 case registry repo、本地 `.state`、`hwpod-cli`、`hwpod-compiler-cli`、`hwlab-api` 和在线 `hwpod-node` 完成单次运行。标准 case registry repo 是 `pikasTech/hwlab-case-registry`,本地默认 checkout 路径是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 只作为迁移前来源。case registry repo 提供 `cases/<caseId>/case.json` 与 HWPOD spec 或 `hwpodId` 引用;每次运行会先在 subject repo 本地 checkout 下创建隔离 Git worktree,再把 `hwpodId` 与该 worktree 路径组合成 `hwpodWorkspaceArgs`。run-local YAML 可作为审计或 runner 后置对照产物保存,但不能作为 Code Agent runner 的注入合同。
|
||||||
|
|
||||||
CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry repo 是 `pikasTech/hwlab-case-registry`,G14 标准 checkout 是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 和 `/root/hwpod-cases` 只作为迁移前来源,不再作为新 CaseRun 入口或写入目标。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、`subject` 引用、期望结果和 `runs/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath` 和 `commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout,`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach <worktree> <commitId>`,不得 clone/fetch GitHub,也不得把 branch、tag 或当前 HEAD 作为执行依据或 fallback。若 HWPOD spec 声明 Keil MDK 工程,准备阶段还必须把同名 `.uvoptx` / `.uvopt` sidecar 从 `repoLocalPath` 同步到 isolated worktree,并在 prepare 结果里暴露 copied/missing 诊断;这些 sidecar 承载 CMSIS-DAP/UV4 探头绑定、flash sequence 和 reset/run 配置,不能被 git worktree 的 tracked-file 视角静默丢失。`subdir` 只描述 subject repo 内的 case 关注区域,不能替代 `spec.workspace.path` 的隔离 worktree。`evidence.json` 必须记录 subject repo 本地路径、subject commit、subdir 和 run-local worktree,避免把 floating ref 或外部脏 workspace 当作可审计证据。
|
CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry repo 是 `pikasTech/hwlab-case-registry`,G14 标准 checkout 是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 和 `/root/hwpod-cases` 只作为迁移前来源,不再作为新 CaseRun 入口或写入目标。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、`subject` 引用、期望结果和 `runs/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath` 和 `commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout,`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach <worktree> <commitId>`,不得 clone/fetch GitHub,也不得把 branch、tag 或当前 HEAD 作为执行依据或 fallback。若 HWPOD spec 声明 Keil MDK 工程,准备阶段还必须把同名 `.uvoptx` / `.uvopt` sidecar 从 `repoLocalPath` 同步到 isolated worktree,并在 prepare 结果里暴露 copied/missing 诊断;这些 sidecar 承载 CMSIS-DAP/UV4 探头绑定、flash sequence 和 reset/run 配置,不能被 git worktree 的 tracked-file 视角静默丢失。`subdir` 只描述 subject repo 内的 case 关注区域,不能替代 `spec.workspace.path` 的隔离 worktree。`evidence.json` 必须记录 subject repo 本地路径、subject commit、subdir 和 run-local worktree,避免把 floating ref 或外部脏 workspace 当作可审计证据。
|
||||||
|
|
||||||
@@ -263,7 +265,7 @@ CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry r
|
|||||||
|
|
||||||
### CaseRun 当前实现边界
|
### CaseRun 当前实现边界
|
||||||
|
|
||||||
当前 `d601-f103-v2-compile` 是 compile-only smoke,用于验证 case registry repo 与 subject repo 分离、`repoLocalPath` 加 `commitId` 的 subject provenance、隔离 subject worktree、run-local spec rewrite 和 D601 Keil 编译证据。它不创建 HWLAB Code Agent session,不调用 DS/DeepSeek/provider,不向 Code Agent 下发任务 prompt,不等待 agent trace,也不验证 agent 在源码仓库内完成修改。因此,compile-only CaseRun 是 HWPOD runner 和 Keil 编译底座 smoke,不是 Code Agent 能力评测。
|
当前 `d601-f103-v2-compile` 是 compile-only smoke,用于验证 case registry repo 与 subject repo 分离、`repoLocalPath` 加 `commitId` 的 subject provenance、隔离 subject worktree、`hwpodId`/workspace binding 和 D601 Keil 编译证据。它不创建 HWLAB Code Agent session,不调用 DS/DeepSeek/provider,不向 Code Agent 下发任务 prompt,不等待 agent trace,也不验证 agent 在源码仓库内完成修改。因此,compile-only CaseRun 是 HWPOD runner 和 Keil 编译底座 smoke,不是 Code Agent 能力评测。
|
||||||
|
|
||||||
### CaseRun 目标边界:布置舞台与任务 prompt
|
### CaseRun 目标边界:布置舞台与任务 prompt
|
||||||
|
|
||||||
@@ -279,7 +281,7 @@ CaseRun 是强化学习 Harness 的最小执行单元,但它本身不是完整
|
|||||||
|
|
||||||
用法详见 `hwlab-caserun` skill,包含同步运行、异步 start/status/result/logs、分步 prepare/build/collect 和 provider profile 选择等完整用法。
|
用法详见 `hwlab-caserun` skill,包含同步运行、异步 start/status/result/logs、分步 prepare/build/collect 和 provider profile 选择等完整用法。
|
||||||
|
|
||||||
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 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/<caseId>/<runId>/`。固定归档包括 `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 定义和流程记录。
|
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree,解析 `hwpodId` 并生成 `hwpodWorkspaceArgs`,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、`hwpodId`、`hwpodWorkspaceArgs`、任务目标和约束交给 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/<caseId>/<runId>/`。固定归档包括 `evidence.json`、`summary.md`、`run.json`、`result.json`、`agent-prompt.md`、`agent-diff.patch`、worker stdout/stderr、`artifact-manifest.json`,以及 `agent-messages.json`、`agent-trace.md`、`agent-transcript.md` 和 `final-response.md`;run-local YAML 若存在,只是审计/对照产物,不是 runner 注入合同。`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 下发给 HWLAB Code Agent 的请求不得再携带 ad hoc `workspaceFiles` 或 source tree 工具种子;当前工具和 skills 必须由 AgentRun `ResourceBundleRef.kind="gitbundle"` 装配,固定把 repo `tools/` 复制到 workspace `tools/`,把 repo `skills/` 复制到 workspace `.agents/skills`。这不是把 prompt 写成答案,而是保证 Code Agent 看到的 `hwpod` 能力、help、wrapper 和 `cmd.run` 组合语义与当前 `v0.2` 源码一致。若 agent 报告标准 `hwpod` 命令缺失或 help 明显过时,优先修 gitbundle runtime assembly 或 HWPOD CLI/skill 装配,不新增 hwpod-node 专用 op,也不把业务动作旁路到 shell 脚本。
|
CaseRun 下发给 HWLAB Code Agent 的请求不得再携带 ad hoc `workspaceFiles` 或 source tree 工具种子;当前工具和 skills 必须由 AgentRun `ResourceBundleRef.kind="gitbundle"` 装配,固定把 repo `tools/` 复制到 workspace `tools/`,把 repo `skills/` 复制到 workspace `.agents/skills`。这不是把 prompt 写成答案,而是保证 Code Agent 看到的 `hwpod` 能力、help、wrapper 和 `cmd.run` 组合语义与当前 `v0.2` 源码一致。若 agent 报告标准 `hwpod` 命令缺失或 help 明显过时,优先修 gitbundle runtime assembly 或 HWPOD CLI/skill 装配,不新增 hwpod-node 专用 op,也不把业务动作旁路到 shell 脚本。
|
||||||
|
|
||||||
@@ -317,9 +319,9 @@ hwlab-cli case aggregate d601-f103-v2-arm2d-integration \
|
|||||||
|
|
||||||
快速阶段最小验收:
|
快速阶段最小验收:
|
||||||
|
|
||||||
1. Code Agent workspace 内存在 `.hwlab/hwpod-spec.yaml`,并能通过 `hwpod-ctl spec validate`。
|
1. `hwpod-ctl spec validate --hwpod-id <id> --workspace-path <run-worktree>` 能从 runtime service resolve HWPOD document,并验证四要素和 node binding。
|
||||||
2. `hwpod-compiler-cli compile` 能把同一个 spec 和高层 intent 编译为稳定 `hwpod-node-ops-v1` plan。
|
2. `hwpod-compiler-cli compile` 能把同一个 `hwpod-id`、workspace override 和高层 intent 编译为稳定 `hwpod-node-ops-v1` plan。
|
||||||
3. `hwpod-cli --dry-run` 输出的 plan 不需要云端 spec,也不读取其他本地旧 profile authority。
|
3. `hwpod-cli --dry-run` 输出的 plan 不读取 runner-local `.hwlab/hwpod-spec.yaml`,也不读取其他本地旧 profile authority。
|
||||||
4. `hwlab-api` 的 node-ops 入口能接收 plan,返回 JSON result;无可用 node 时必须返回结构化 blocker,而不是静默伪造 DEV-LIVE。
|
4. `hwlab-api` 的 node-ops 入口能接收 plan,返回 JSON result;无可用 node 时必须返回结构化 blocker,而不是静默伪造 DEV-LIVE。
|
||||||
5. 真实 CLI 验收必须证明 `hwpod-cli -> hwpod-compiler-cli -> hwlab-api /v1/hwpod-node-ops -> hwpod-node` 全链路走通。
|
5. 真实 CLI 验收必须证明 `hwpod-cli -> hwpod-compiler-cli -> hwlab-api /v1/hwpod-node-ops -> hwpod-node` 全链路走通。
|
||||||
6. CaseRun 流程必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath` 和 `subject.commitId` 创建隔离 subject worktree,生成 Code Agent prompt/context,记录 agent session/trace/conversation/provider,采集 subject worktree diff,执行 compile-only smoke,并产出 isolated run state 和 case registry repo 全量产物归档;当前版本只记录流程证据和 trace manifest,不设置通过条件、不自动评价 evidence,并明确记录 downloadSkipped。
|
6. CaseRun 流程必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath` 和 `subject.commitId` 创建隔离 subject worktree,生成 Code Agent prompt/context,记录 agent session/trace/conversation/provider,采集 subject worktree diff,执行 compile-only smoke,并产出 isolated run state 和 case registry repo 全量产物归档;当前版本只记录流程证据和 trace manifest,不设置通过条件、不自动评价 evidence,并明确记录 downloadSkipped。
|
||||||
@@ -327,5 +329,5 @@ hwlab-cli case aggregate d601-f103-v2-arm2d-integration \
|
|||||||
8. `d601-f103-v2-compile` 的真实流程只覆盖当前编排链路和 compile-only smoke;操作员直接运行 `hwpod-cli build`、直接调用 Keil 或只检查 node 侧 job,不等同于 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 是否完成任务。
|
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 <caseId> --run-id <runId>` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diff;Trace 中 message 直接展示,工具调用用 `已运行 <command>` summary 折叠。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。
|
10. 已完成 run 的 registry 聚合验收必须通过 `hwlab-cli case aggregate <caseId> --run-id <runId>` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diff;Trace 中 message 直接展示,工具调用用 `已运行 <command>` summary 折叠。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。
|
||||||
11. Windows subject worktree 文本编辑必须能在 CRLF 文件中通过 `workspace.insert-after` 或 `workspace.replace` 完成一行源码修改,返回 before/after SHA 与 diff 摘要,并保留原文件 CRLF;`workspace.apply-patch` 在 context 不匹配时必须返回可诊断 payload,而不是只给 `context not found`。
|
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。
|
12. Keil `debug.download` 自动装配必须输出结构化 `cmd.run` argv 步骤;带空格的 `probeName` 在 plan 中必须是单独 argv 元素。`io.uart.read` 必须通过节点本地 `serial-monitor` 读取正在监控的 COM/baud;未监控或工具失败时必须返回包含 `ioProbe`/COM 口/baud/monitor status/启动建议的结构化 blocker details。
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Use the repo-local resource bundle as the source of runtime rules:
|
|||||||
|
|
||||||
- Required runtime files are mounted from `ResourceBundleRef.kind="gitbundle"`: repo `tools/` is copied to workspace `tools/`, and repo `skills/` is copied to workspace `.agents/skills` for this run.
|
- Required runtime files are mounted from `ResourceBundleRef.kind="gitbundle"`: repo `tools/` is copied to workspace `tools/`, and repo `skills/` is copied to workspace `.agents/skills` for this run.
|
||||||
- The expected HWLAB skills are `hwpod-cli`, `hwpod-ctl`, and `hwlab-agent-runtime`.
|
- The expected HWLAB skills are `hwpod-cli`, `hwpod-ctl`, and `hwlab-agent-runtime`.
|
||||||
- Use `hwpod` from PATH for HWPOD work. It is the standard `hwpod-cli` entry and must read `.hwlab/hwpod-spec.yaml`, call `hwpod-compiler-cli`, submit `/v1/hwpod-node-ops`, and wait for `hwpod-node` results.
|
- Use `hwpod` from PATH for HWPOD work. It is the standard `hwpod-cli` entry and must resolve HWPOD definitions by `hwpod-id` through the runtime service, submit `/v1/hwpod-node-ops`, and wait for `hwpod-node` results.
|
||||||
- The runner workspace should include a preinstalled `.hwlab/hwpod-spec.yaml`. When the user asks which HWPOD or `hwpod-spec` is available, first query `${HWLAB_RUNTIME_API_URL}/v1/hwpod/specs?probe=1` or validate the preinstalled spec; report the discovered spec count and names before explaining concepts.
|
- The runner workspace is not required to contain `.hwlab/hwpod-spec.yaml`. When the user asks which HWPOD or `hwpod-spec` is available, query `${HWLAB_RUNTIME_API_URL}/v1/hwpod/specs?probe=1`; report the discovered spec count and names before explaining concepts.
|
||||||
- Use `unidesk-ssh` only for UniDesk passthrough tasks that are not covered by HWPOD APIs.
|
- Use `unidesk-ssh` only for UniDesk passthrough tasks that are not covered by HWPOD APIs.
|
||||||
|
|
||||||
Do not use fallback execution paths:
|
Do not use fallback execution paths:
|
||||||
@@ -29,6 +29,6 @@ MiniMax-M3 tool-call guidance:
|
|||||||
- For GitHub issue/PR reads, prefer `gh issue view <number> --repo owner/name --json title,body,state,comments` or `gh pr view ... --json ...`; avoid plain `gh issue view` output that can be polluted by Projects Classic GraphQL warnings.
|
- For GitHub issue/PR reads, prefer `gh issue view <number> --repo owner/name --json title,body,state,comments` or `gh pr view ... --json ...`; avoid plain `gh issue view` output that can be polluted by Projects Classic GraphQL warnings.
|
||||||
- If a command fails because the command arguments were malformed, report the malformed command and retry once with a shorter single-purpose command.
|
- If a command fails because the command arguments were malformed, report the malformed command and retry once with a shorter single-purpose command.
|
||||||
|
|
||||||
When the user asks to compile or operate hardware such as `D601-F103-V2`, start from the HWPOD path: validate `.hwlab/hwpod-spec.yaml` with `hwpod-ctl`, run `hwpod inspect`, then use `hwpod build` / `hwpod download` / `hwpod reset` as needed. If HWPOD returns a named blocker, report and fix that blocker in the HWPOD path.
|
When the user asks to compile or operate hardware such as `D601-F103-V2`, start from the HWPOD path. If the task prompt provides `hwpodWorkspaceArgs`, reuse it exactly with `hwpod-ctl spec validate`, `hwpod inspect`, `hwpod build`, `hwpod download`, `hwpod reset`, and `hwpod job status`. Otherwise discover a `hwpod-id` through the runtime service and pass `--hwpod-id <id>` plus the task workspace path when one is provided. If HWPOD returns a named blocker, report and fix that blocker in the HWPOD path.
|
||||||
|
|
||||||
When `.hwlab/hwpod-spec.yaml` is missing, do not answer that zero HWPODs are available until `/v1/hwpod/specs?probe=1` also returns an empty list. If discovery returns a spec, initialize or restore the workspace-local spec from that preinstalled/registry source and continue through the HWPOD path.
|
When `.hwlab/hwpod-spec.yaml` is missing, do not initialize, copy, restore, or repair a runner-local spec. Use `/v1/hwpod/specs?probe=1` and the task `hwpod-id`/`hwpodWorkspaceArgs` instead. Only use `--spec <path>` when the user explicitly asks to debug or import a local YAML export.
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
|
|||||||
});
|
});
|
||||||
let runnerJob = null;
|
let runnerJob = null;
|
||||||
try {
|
try {
|
||||||
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile, params });
|
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile });
|
||||||
runnerJob = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/runner-jobs`, {
|
runnerJob = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/runner-jobs`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: runnerJobInput,
|
body: runnerJobInput,
|
||||||
@@ -386,7 +386,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
|
|||||||
message: "AgentRun command " + commandId + " created; hwlab-cloud-api will start a runner Job explicitly without relying on scheduler automation.",
|
message: "AgentRun command " + commandId + " created; hwlab-cloud-api will start a runner Job explicitly without relying on scheduler automation.",
|
||||||
runId, commandId, backendProfile, waitingFor: "agentrun-runner-job-create", valuesPrinted: false,
|
runId, commandId, backendProfile, waitingFor: "agentrun-runner-job-create", valuesPrinted: false,
|
||||||
});
|
});
|
||||||
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile, params });
|
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile });
|
||||||
try {
|
try {
|
||||||
runnerJob = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs/" + encodeURIComponent(runId) + "/runner-jobs", { method: "POST", body: runnerJobInput, timeoutMs });
|
runnerJob = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs/" + encodeURIComponent(runId) + "/runner-jobs", { method: "POST", body: runnerJobInput, timeoutMs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -919,8 +919,8 @@ async function resolveOwnerApiKey({ params, options, now }) {
|
|||||||
if (!key) return "";
|
if (!key) return "";
|
||||||
return text(key.displaySecret ?? "");
|
return text(key.displaySecret ?? "");
|
||||||
}
|
}
|
||||||
function buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities = null, backendProfile, params = {} }) {
|
function buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities = null, backendProfile }) {
|
||||||
const baseTransient = buildAgentRunTransientEnv(env, { providerProfile: backendProfile, parentTraceId: traceId, hwpodSpecContent: params.hwpodSpecContent });
|
const baseTransient = buildAgentRunTransientEnv(env, { providerProfile: backendProfile, parentTraceId: traceId });
|
||||||
const hwpodAllowed = toolCapabilityAllowed(toolCapabilities, "hwpod");
|
const hwpodAllowed = toolCapabilityAllowed(toolCapabilities, "hwpod");
|
||||||
const transientEnv = ownerApiKey && hwpodAllowed
|
const transientEnv = ownerApiKey && hwpodAllowed
|
||||||
? baseTransient.concat([{ name: "HWLAB_API_KEY", value: ownerApiKey, sensitive: true }])
|
? baseTransient.concat([{ name: "HWLAB_API_KEY", value: ownerApiKey, sensitive: true }])
|
||||||
@@ -942,7 +942,6 @@ function buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, too
|
|||||||
function buildAgentRunTransientEnv(env = process.env, options = {}) {
|
function buildAgentRunTransientEnv(env = process.env, options = {}) {
|
||||||
const providerProfile = firstNonEmpty(options.providerProfile);
|
const providerProfile = firstNonEmpty(options.providerProfile);
|
||||||
const parentTraceId = firstNonEmpty(options.parentTraceId);
|
const parentTraceId = firstNonEmpty(options.parentTraceId);
|
||||||
const hwpodSpecContent = hwpodSpecContentForEnv(options.hwpodSpecContent);
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const name of [
|
for (const name of [
|
||||||
"HWLAB_RUNTIME_API_URL",
|
"HWLAB_RUNTIME_API_URL",
|
||||||
@@ -958,18 +957,9 @@ function buildAgentRunTransientEnv(env = process.env, options = {}) {
|
|||||||
}
|
}
|
||||||
if (providerProfile) entries.push({ name: "HWLAB_CODE_AGENT_PROVIDER_PROFILE", value: providerProfile, sensitive: false });
|
if (providerProfile) entries.push({ name: "HWLAB_CODE_AGENT_PROVIDER_PROFILE", value: providerProfile, sensitive: false });
|
||||||
if (parentTraceId) entries.push({ name: "HWLAB_CODE_AGENT_PARENT_TRACE_ID", value: parentTraceId, sensitive: false });
|
if (parentTraceId) entries.push({ name: "HWLAB_CODE_AGENT_PARENT_TRACE_ID", value: parentTraceId, sensitive: false });
|
||||||
if (hwpodSpecContent) entries.push({ name: "HWPOD_SPEC_CONTENT", value: `base64:${Buffer.from(hwpodSpecContent, "utf8").toString("base64")}`, sensitive: false });
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hwpodSpecContentForEnv(value) {
|
|
||||||
const content = typeof value === "string" ? value.trim() : "";
|
|
||||||
if (!content) return "";
|
|
||||||
if (Buffer.byteLength(content, "utf8") > 64 * 1024) throw adapterError("hwpod_spec_too_large", "hwpodSpecContent must be 64 KiB or smaller");
|
|
||||||
if (!/^apiVersion:/mu.test(content) || !/\nkind:\s*Hwpod\b/mu.test(content)) throw adapterError("invalid_hwpod_spec_content", "hwpodSpecContent must be a Hwpod YAML document");
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function agentRunToolCredentials(env = process.env, toolCapabilities = null) {
|
function agentRunToolCredentials(env = process.env, toolCapabilities = null) {
|
||||||
const namespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TOOL_SECRET_NAMESPACE, env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
|
const namespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TOOL_SECRET_NAMESPACE, env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
|
||||||
const githubSecretName = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME, "agentrun-v01-tool-github-pr");
|
const githubSecretName = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME, "agentrun-v01-tool-github-pr");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: hwpod-cli
|
name: hwpod-cli
|
||||||
description: Use HWLAB HWPOD from a code agent runner for hardware-development tasks. The hwpod-cli skill reads workspace-local hwpod-spec, calls hwpod-compiler-cli, submits hwpod-node-ops through hwlab-api, and waits for hwpod-node results.
|
description: Use HWLAB HWPOD from a code agent runner for hardware-development tasks. The hwpod-cli skill resolves an HWPOD by hwpod-id through the HWLAB runtime service, submits hwpod-node-ops through hwlab-api, and waits for hwpod-node results.
|
||||||
---
|
---
|
||||||
|
|
||||||
# hwpod-cli
|
# hwpod-cli
|
||||||
@@ -12,27 +12,29 @@ This skill follows Skill(cli-spec). It is the standard task entry for operating
|
|||||||
```text
|
```text
|
||||||
hwpod from PATH
|
hwpod from PATH
|
||||||
-> tools/hwpod-cli.ts
|
-> tools/hwpod-cli.ts
|
||||||
-> tools/hwpod-compiler-cli.ts
|
-> runtime HWPOD registry by hwpod-id
|
||||||
|
-> compiler-backed hwpod-node-ops plan
|
||||||
-> /v1/hwpod-node-ops on hwlab-api
|
-> /v1/hwpod-node-ops on hwlab-api
|
||||||
-> hwpod-node on the host side
|
-> hwpod-node on the host side
|
||||||
```
|
```
|
||||||
|
|
||||||
The HWPOD spec is code-agent-side state. The default path is `.hwlab/hwpod-spec.yaml`; pass `--spec <path>` only when a task explicitly uses another workspace-local spec.
|
The HWPOD definition is runtime service state. The standard runner contract is `--hwpod-id <id>` plus `--workspace-path <run-worktree>` when the task is bound to a run-local workspace. A task may provide the complete `hwpodWorkspaceArgs` string; reuse it exactly. `--spec <path>` is only an explicit debug/import path for a local exported YAML, not the default runner contract.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- Inspect the HWPOD path: `hwpod inspect --spec .hwlab/hwpod-spec.yaml`.
|
- Inspect the HWPOD path: `hwpod inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree>`.
|
||||||
- Workspace search/read/edit: use `hwpod workspace ls|cat|read|rg|grep|search|write|replace|insert-after|apply-patch --spec .hwlab/hwpod-spec.yaml`. `read` is an alias for `cat`; `grep` and `search` are aliases for `rg`; `--file` and `--remote-path` are accepted as path aliases for file-oriented commands.
|
- Workspace search/read/edit: use `hwpod workspace ls|cat|read|rg|grep|search|apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree>`. `read` is an alias for `cat`; `grep` and `search` are aliases for `rg`; `--file` and `--remote-path` are accepted as path aliases for file-oriented commands.
|
||||||
- For symbol lookup or ARM-2D header hunting, prefer `hwpod workspace rg <pattern> [path] --context 3 --spec .hwlab/hwpod-spec.yaml` instead of `cat | grep`.
|
- For symbol lookup or ARM-2D header hunting, prefer `hwpod workspace rg <pattern> [path] --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>` instead of `cat | grep`.
|
||||||
- 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.
|
- 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 separate short `hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml` calls instead of shell polling wrappers such as `sleep &&`, `timeout`, `watch`, `head`, pipes, or loops.
|
- Keil async status: after build/download returns a job id, use separate short `hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>` calls instead of shell polling wrappers such as `sleep &&`, `timeout`, `watch`, `head`, pipes, or loops.
|
||||||
- UART read: use `hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1`; the compiler runs the configured serial-monitor CLI through `cmd.run`.
|
- UART read: use `hwpod uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --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
|
## 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 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`.
|
- Do not put full `hwpod-spec` on `hwpod-node`. The node only accepts `hwpod-node-ops`.
|
||||||
|
- Do not create, copy, restore, or repair runner-local `.hwlab/hwpod-spec.yaml` as a workaround. Use `hwpod-id` through the runtime service.
|
||||||
|
- Do not use deprecated workspace mutation commands (`workspace write`, `replace`, or `insert-after`) for normal source edits. Use `workspace apply-patch`.
|
||||||
- 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.
|
- 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.
|
- Use workspace file operations for subject text edits. Do not use PowerShell/cmd heredocs for normal source edits unless debugging `cmd.run` itself.
|
||||||
- For multi-line edits, prefer `hwpod workspace apply-patch` with a Codex/UniDesk apply_patch v2 envelope from stdin or `--patch-content`. Put file paths in `*** Update File:` / `*** Add File:` / `*** Delete File:` headers. Do not pass raw unified diff (`---`, `+++`, `@@ -N`) as the whole patch; HWPOD deliberately rejects that format with a clear hint.
|
- For multi-line edits, prefer `hwpod workspace apply-patch` with a Codex/UniDesk apply_patch v2 envelope from stdin or `--patch-content`. Put file paths in `*** Update File:` / `*** Add File:` / `*** Delete File:` headers. Do not pass raw unified diff (`---`, `+++`, `@@ -N`) as the whole patch; HWPOD deliberately rejects that format with a clear hint.
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
---
|
---
|
||||||
name: hwpod-ctl
|
name: hwpod-ctl
|
||||||
description: Manage the workspace-local HWLAB hwpod-spec and HWPOD node binding from a code agent runner. The hwpod-ctl skill is separate from hwpod-cli and is used for spec editing, validation, smoke checks, and temporary maintenance.
|
description: Manage and validate HWLAB HWPOD runtime bindings from a code agent runner. The hwpod-ctl skill is separate from hwpod-cli and is used for hwpod-id validation, smoke checks, and explicit local spec import/export maintenance.
|
||||||
---
|
---
|
||||||
|
|
||||||
# hwpod-ctl
|
# hwpod-ctl
|
||||||
|
|
||||||
This skill follows Skill(cli-spec). It is the standard management entry for the workspace-local HWPOD spec and node binding.
|
This skill follows Skill(cli-spec). It is the standard management entry for runtime-resolved HWPOD identity and node binding checks.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
`hwpod-ctl` is a peer of `hwpod-cli`, not a fallback task path. Use it to maintain `.hwlab/hwpod-spec.yaml`, validate the four HWPOD elements, bind a node, and run smoke or temporary maintenance actions.
|
`hwpod-ctl` is a peer of `hwpod-cli`, not a fallback task path. Use it to validate a service-resolved `hwpod-id`, inspect the four HWPOD elements, check node binding, and run smoke or temporary maintenance actions.
|
||||||
|
|
||||||
`hwpod-ctl` does not replace `hwpod-cli` for task execution. When a task needs source lookup, use `hwpod workspace rg/search` for symbol hunting and `hwpod workspace cat` only for full file bodies; when a task needs build/download/UART/job status, hand it back to `hwpod-cli` and its compiler-backed plan path.
|
`hwpod-ctl` does not replace `hwpod-cli` for task execution. When a task needs source lookup, use `hwpod workspace rg/search` for symbol hunting and `hwpod workspace cat` only for full file bodies; when a task needs build/download/UART/job status, hand it back to `hwpod-cli` and its compiler-backed plan path.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- Initialize or repair local spec: `hwpod-ctl spec init --spec .hwlab/hwpod-spec.yaml --node <nodeId>`.
|
- Validate runtime HWPOD before hardware work: `hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>`.
|
||||||
- Validate local spec before hardware work: `hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml`.
|
- Show the resolved HWPOD document: `hwpod-ctl spec show --hwpod-id d601-f103-v2 --workspace-path <run-worktree>`.
|
||||||
- Edit fields: `hwpod-ctl spec set <dot.path> <value> --spec .hwlab/hwpod-spec.yaml`.
|
- For explicit debug/import only, local YAML can still be passed with `--spec <path>`; do not use this as a runner default.
|
||||||
- Bind a node: `hwpod-ctl bind --spec .hwlab/hwpod-spec.yaml --node <nodeId>`.
|
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- `hwpod-ctl` owns workspace-local spec management during the fast iteration stage; cloud-side HWPOD spec authority is a later stage.
|
- `hwpod-ctl` must not initialize or restore runner-local `.hwlab/hwpod-spec.yaml` to make a task pass. HWPOD authority is the runtime service and `hwpod-id`.
|
||||||
- `hwpod-node` must not receive or store the full `hwpod-spec`.
|
- `hwpod-node` must not receive or store the full `hwpod-spec`.
|
||||||
- Temporary maintenance commands belong here, not in normal `hwpod-cli` task flows.
|
- Temporary maintenance commands belong here, not in normal `hwpod-cli` task flows.
|
||||||
|
|||||||
@@ -473,18 +473,21 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
|
|||||||
assert.doesNotMatch(prompt, /path: "F:\\\\Work\\\\HWLAB-CASE-F103/u);
|
assert.doesNotMatch(prompt, /path: "F:\\\\Work\\\\HWLAB-CASE-F103/u);
|
||||||
assert.doesNotMatch(prompt, /CaseRun has already placed the run-local spec/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.doesNotMatch(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u);
|
||||||
|
assert.match(prompt, /hwpodId: d601-f103-v2/u);
|
||||||
|
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, /standard hwpod\/hwpod-ctl commands/u);
|
||||||
assert.match(prompt, /hwpod job status <jobId>/u);
|
assert.match(prompt, /hwpod job status <jobId>/u);
|
||||||
assert.match(prompt, /不要再用 shell sleep\/&&\/timeout\/watch\/head\/pipe/u);
|
assert.match(prompt, /不要再用 shell sleep\/&&\/timeout\/watch\/head\/pipe/u);
|
||||||
assert.match(prompt, /separate short `hwpod job status <jobId>` commands/u);
|
assert.match(prompt, /separate short `hwpod job status <jobId> --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");
|
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.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/sessions"), true);
|
||||||
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat"), 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");
|
const agentChatRequest = requests.find((item) => item.url === "http://web.test/v1/agent/chat");
|
||||||
assert.equal(Object.hasOwn(agentChatRequest.body, "workspaceFiles"), false);
|
assert.equal(Object.hasOwn(agentChatRequest.body, "workspaceFiles"), false);
|
||||||
assert.equal(typeof agentChatRequest.body.hwpodSpecContent, "string");
|
assert.equal(Object.hasOwn(agentChatRequest.body, "hwpodSpecContent"), false);
|
||||||
assert.match(agentChatRequest.body.hwpodSpecContent, /HWLAB-CASE-F103.*caserun-run-agent-flow/u);
|
|
||||||
assert.doesNotMatch(agentChatRequest.body.message, /path: "F:\\Work\\HWLAB-CASE-F103\\\.worktree\\caserun-run-agent-flow"/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/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);
|
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/trace/" + result.payload.agent.traceId), false);
|
||||||
|
|||||||
+49
-40
@@ -45,43 +45,51 @@ test("hwpod-ctl initializes, validates, edits, and binds workspace-local hwpod-s
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hwpod cli materializes workspace spec from env", async () => {
|
test("hwpod cli resolves hwpod-id from runtime discovery and applies workspace override", async () => {
|
||||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-env-spec-"));
|
const runWorkspace = "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-hwpod-id";
|
||||||
const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml");
|
const requests: any[] = [];
|
||||||
const content = [
|
const fetchImpl: any = async (url: string, init: any) => {
|
||||||
"apiVersion: hwlab.dev/v0alpha1",
|
requests.push({ url, init });
|
||||||
"kind: Hwpod",
|
return {
|
||||||
"metadata:",
|
status: 200,
|
||||||
" uid: D601-F103-V2",
|
json: async () => ({
|
||||||
" name: d601-f103-v2",
|
ok: true,
|
||||||
"spec:",
|
status: "completed",
|
||||||
" targetDevice:",
|
specs: [{
|
||||||
" board: D601-F103-V2",
|
hwpodId: "d601-f103-v2",
|
||||||
" mcu: STM32F103",
|
authority: "preinstalled-verified-spec",
|
||||||
" workspace:",
|
document: {
|
||||||
" path: F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-env-spec",
|
apiVersion: "hwlab.dev/v0alpha1",
|
||||||
" debugProbe:",
|
kind: "Hwpod",
|
||||||
" type: daplink",
|
metadata: { uid: "D601-F103-V2", name: "d601-f103-v2" },
|
||||||
" ioProbe:",
|
spec: {
|
||||||
" uart:",
|
targetDevice: { board: "D601-F103-V2", mcu: "STM32F103" },
|
||||||
" id: uart/1",
|
workspace: { path: "F:\\Work\\D601-HWLAB" },
|
||||||
" nodeBinding:",
|
debugProbe: { type: "daplink" },
|
||||||
" nodeId: node-d601-f103-v2",
|
ioProbe: { uart: { id: "uart/1" } },
|
||||||
""
|
nodeBinding: { nodeId: "node-d601-f103-v2" }
|
||||||
].join("\n");
|
}
|
||||||
try {
|
}
|
||||||
const encoded = `base64:${Buffer.from(content, "utf8").toString("base64")}`;
|
}]
|
||||||
const validate = await runHwpodCtl(["spec", "validate", "--spec", specPath], { now: () => NOW, env: { HWPOD_SPEC_CONTENT: encoded } });
|
})
|
||||||
assert.equal(validate.exitCode, 0);
|
};
|
||||||
assert.equal(validate.payload.document.metadata.name, "d601-f103-v2");
|
};
|
||||||
assert.match(await readFile(specPath, "utf8"), /caserun-env-spec/u);
|
|
||||||
|
|
||||||
const plan = await runHwpodCli(["workspace", "ls", ".", "--spec", specPath, "--dry-run"], { now: () => NOW, env: { HWPOD_SPEC_CONTENT: encoded } });
|
const env = { HWLAB_RUNTIME_API_URL: "http://api.test" };
|
||||||
assert.equal(plan.exitCode, 0);
|
const validate = await runHwpodCtl(["spec", "validate", "--hwpod-id", "d601-f103-v2", "--workspace-path", runWorkspace], { now: () => NOW, env, fetchImpl });
|
||||||
assert.equal(plan.payload.plan.resourceHints.workspacePath, "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-env-spec");
|
assert.equal(validate.exitCode, 0);
|
||||||
} finally {
|
assert.equal(validate.payload.specPath, "hwpod://d601-f103-v2");
|
||||||
await rm(root, { recursive: true, force: true });
|
assert.equal(validate.payload.specAuthority, "preinstalled-verified-spec");
|
||||||
}
|
assert.equal(validate.payload.document.spec.workspace.path, runWorkspace);
|
||||||
|
|
||||||
|
const plan = await runHwpodCli(["workspace", "ls", ".", "--hwpod-id", "d601-f103-v2", "--workspace-path", runWorkspace, "--dry-run"], { now: () => NOW, env, fetchImpl });
|
||||||
|
assert.equal(plan.exitCode, 0);
|
||||||
|
assert.equal(plan.payload.hwpodId, "d601-f103-v2");
|
||||||
|
assert.equal(plan.payload.specPath, "hwpod://d601-f103-v2");
|
||||||
|
assert.equal(plan.payload.plan.resourceHints.workspacePath, runWorkspace);
|
||||||
|
assert.equal(plan.payload.plan.source.specAuthority, "preinstalled-verified-spec");
|
||||||
|
assert.equal(plan.payload.plan.ops[0].args.workspacePath, runWorkspace);
|
||||||
|
assert.match(requests[0].url, /\/v1\/hwpod\/specs\?hwpodId=d601-f103-v2/u);
|
||||||
});
|
});
|
||||||
test("hwpod-compiler-cli compiles workspace-local spec into node ops", async () => {
|
test("hwpod-compiler-cli compiles workspace-local spec into node ops", async () => {
|
||||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-compiler-"));
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-compiler-"));
|
||||||
@@ -189,14 +197,14 @@ test("hwpod-cli dry-run invokes hwpod-compiler-cli and exposes hwpod-node-ops pl
|
|||||||
assert.equal(inspect.exitCode, 0);
|
assert.equal(inspect.exitCode, 0);
|
||||||
assert.equal(inspect.payload.dryRun, true);
|
assert.equal(inspect.payload.dryRun, true);
|
||||||
assert.equal(inspect.payload.compilerInvocation.compiler, "hwpod-compiler-cli");
|
assert.equal(inspect.payload.compilerInvocation.compiler, "hwpod-compiler-cli");
|
||||||
assert.equal(inspect.payload.compilerInvocation.mode, "subprocess");
|
assert.equal(inspect.payload.compilerInvocation.mode, "in-process");
|
||||||
assert.equal(inspect.payload.compilerInvocation.action, "hwpod-compiler.compile");
|
assert.equal(inspect.payload.compilerInvocation.action, "hwpod-compiler.compile");
|
||||||
assert.equal(inspect.payload.plan.source.compiler, "hwpod-compiler-cli");
|
assert.equal(inspect.payload.plan.source.compiler, "hwpod-compiler-cli");
|
||||||
assert.deepEqual(inspect.payload.plan.ops.map((op: any) => op.op), ["node.health", "node.inventory"]);
|
assert.deepEqual(inspect.payload.plan.ops.map((op: any) => op.op), ["node.health", "node.inventory"]);
|
||||||
|
|
||||||
const build = await runHwpodCli(["build", "--spec", specPath, "--dry-run", "--target", "Debug", "--command", "printf cli-build", "--reason", "target flow smoke"], { now: () => NOW });
|
const build = await runHwpodCli(["build", "--spec", specPath, "--dry-run", "--target", "Debug", "--command", "printf cli-build", "--reason", "target flow smoke"], { now: () => NOW });
|
||||||
assert.equal(build.exitCode, 0);
|
assert.equal(build.exitCode, 0);
|
||||||
assert.equal(build.payload.compilerInvocation.mode, "subprocess");
|
assert.equal(build.payload.compilerInvocation.mode, "in-process");
|
||||||
assert.equal(build.payload.plan.ops[0].op, "debug.build");
|
assert.equal(build.payload.plan.ops[0].op, "debug.build");
|
||||||
assert.equal(build.payload.plan.ops[0].args.target, "Debug");
|
assert.equal(build.payload.plan.ops[0].args.target, "Debug");
|
||||||
assert.equal(build.payload.plan.ops[0].args.command, "printf cli-build");
|
assert.equal(build.payload.plan.ops[0].args.command, "printf cli-build");
|
||||||
@@ -276,7 +284,8 @@ test("hwpod-cli exposes subcommand help before required args and accepts workspa
|
|||||||
const workspaceHelp = await runHwpodCli(["workspace", "--help"], { now: () => NOW });
|
const workspaceHelp = await runHwpodCli(["workspace", "--help"], { now: () => NOW });
|
||||||
assert.equal(workspaceHelp.exitCode, 0);
|
assert.equal(workspaceHelp.exitCode, 0);
|
||||||
assert.equal(workspaceHelp.payload.action, "hwpod-cli.workspace.help");
|
assert.equal(workspaceHelp.payload.action, "hwpod-cli.workspace.help");
|
||||||
assert.match(JSON.stringify(workspaceHelp.payload.usage), /workspace cat --file <path>/u);
|
assert.match(JSON.stringify(workspaceHelp.payload.usage), /--hwpod-id d601-f103-v2/u);
|
||||||
|
assert.match(JSON.stringify(workspaceHelp.payload.usage), /--workspace-path <run-worktree>/u);
|
||||||
|
|
||||||
const writeHelp = await runHwpodCli(["workspace", "write", "--help"], { now: () => NOW });
|
const writeHelp = await runHwpodCli(["workspace", "write", "--help"], { now: () => NOW });
|
||||||
assert.equal(writeHelp.exitCode, 0);
|
assert.equal(writeHelp.exitCode, 0);
|
||||||
@@ -324,7 +333,7 @@ test("hwpod-cli submits compiled node ops to hwlab-api when not dry-run", async
|
|||||||
});
|
});
|
||||||
assert.equal(result.exitCode, 0);
|
assert.equal(result.exitCode, 0);
|
||||||
assert.equal(result.payload.compilerInvocation.compiler, "hwpod-compiler-cli");
|
assert.equal(result.payload.compilerInvocation.compiler, "hwpod-compiler-cli");
|
||||||
assert.equal(result.payload.compilerInvocation.mode, "subprocess");
|
assert.equal(result.payload.compilerInvocation.mode, "in-process");
|
||||||
assert.equal(seen[0].url, "http://cloud.test/v1/hwpod-node-ops");
|
assert.equal(seen[0].url, "http://cloud.test/v1/hwpod-node-ops");
|
||||||
assert.equal(seen[0].init.headers.authorization, "Bearer hwl_live_test");
|
assert.equal(seen[0].init.headers.authorization, "Bearer hwl_live_test");
|
||||||
assert.equal(seen[0].body.contractVersion, "hwpod-node-ops-v1");
|
assert.equal(seen[0].body.contractVersion, "hwpod-node-ops-v1");
|
||||||
|
|||||||
@@ -749,8 +749,7 @@ async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
|
|||||||
const threadId = session.threadId;
|
const threadId = session.threadId;
|
||||||
const traceId = text(context.parsed.traceId) || `trc_case_${slug(run.caseId)}_${randomUUID().replace(/-/gu, "")}`;
|
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) }) });
|
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 hwpodSpecContent = await readFile(run.specPath, "utf8");
|
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt });
|
||||||
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt, hwpodSpecContent });
|
|
||||||
if (!accepted.ok) {
|
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 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 });
|
const nextRun = await updateRun(context, run, { agent });
|
||||||
@@ -881,7 +880,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; hwpodSpecContent?: string }) {
|
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`, {
|
return caseFetchJson(context, `${input.baseUrl}/v1/agent/chat`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...caseAgentHeaders(context), "x-trace-id": input.traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" },
|
headers: { ...caseAgentHeaders(context), "x-trace-id": input.traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" },
|
||||||
@@ -892,7 +891,6 @@ async function submitAgentTask(context: CaseContext, input: { baseUrl: string; p
|
|||||||
threadId: input.threadId,
|
threadId: input.threadId,
|
||||||
traceId: input.traceId,
|
traceId: input.traceId,
|
||||||
providerProfile: input.providerProfile,
|
providerProfile: input.providerProfile,
|
||||||
hwpodSpecContent: input.hwpodSpecContent,
|
|
||||||
shortConnection: true,
|
shortConnection: true,
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
updatedByClient: "hwlab-cli.case-run"
|
updatedByClient: "hwlab-cli.case-run"
|
||||||
@@ -1078,15 +1076,20 @@ async function updateRun(context: CaseContext, run: PreparedCaseRun, patch: Part
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAgentTask) {
|
async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAgentTask) {
|
||||||
|
const hwpodDocument = await readHwpodSpec(run.specPath);
|
||||||
|
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 = [
|
const constraints = [
|
||||||
...agentTask.constraints,
|
...agentTask.constraints,
|
||||||
"只能修改 isolated subject worktree,不得修改 case registry repo。",
|
"只能修改 isolated subject worktree,不得修改 case registry repo。",
|
||||||
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
|
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
|
||||||
"AgentRun 通过 kind=gitbundle 装配当前 v0.2 的 tools/ 与 .agents/skills;若标准 hwpod 命令能力缺失,报告 gitbundle runtime assembly 问题,不要改走旁路。",
|
"AgentRun 通过 kind=gitbundle 装配当前 v0.2 的 tools/ 与 .agents/skills;若标准 hwpod 命令能力缺失,报告 gitbundle runtime assembly 问题,不要改走旁路。",
|
||||||
"不要依赖 workspaceFiles、seed files、hostPath skill 目录或 ConfigMap 作为工具/skill 注入 fallback。",
|
"不要依赖 workspaceFiles、seed files、hostPath skill 目录、ConfigMap 或 runner 本地 .hwlab/hwpod-spec.yaml 作为工具/skill/HWPOD 注入 fallback。",
|
||||||
|
`必须通过 HWPOD registry/service 引用 hwpodId=${hwpodId};所有 hwpod/hwpod-ctl 命令都携带 ${hwpodWorkspaceArgs}。`,
|
||||||
|
"如果 case prompt 或旧帮助文本提到 .hwlab/hwpod-spec.yaml,把它视为过时写法并替换为本任务给出的 --hwpod-id/--workspace-path 参数;不要创建、复制或修补本地 spec 文件。",
|
||||||
"若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。",
|
"若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。",
|
||||||
"不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。",
|
"不要运行 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 短命令做有限轮询。"
|
`hwpod build/download 是长任务短连接入口;不要再用 shell sleep/&&/timeout/watch/head/pipe 或 shell loop 包住它们。记录返回 JSON 里的 jobId/job_id,再用独立的 hwpod job status <jobId> ${hwpodWorkspaceArgs} 短命令做有限轮询。`
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return [
|
return [
|
||||||
`# HWPOD CaseRun Code Agent Task`,
|
`# HWPOD CaseRun Code Agent Task`,
|
||||||
@@ -1096,11 +1099,22 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
|
|||||||
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
|
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
|
||||||
`subjectCommitId: ${run.subject.commitId}`,
|
`subjectCommitId: ${run.subject.commitId}`,
|
||||||
`subjectWorktreePath: ${run.subject.worktreePath}`,
|
`subjectWorktreePath: ${run.subject.worktreePath}`,
|
||||||
|
`hwpodId: ${hwpodId}`,
|
||||||
|
`hwpodWorkspaceArgs: ${hwpodWorkspaceArgs}`,
|
||||||
`verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`,
|
`verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`,
|
||||||
``,
|
``,
|
||||||
`## Runtime Assembly`,
|
`## 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 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.`,
|
||||||
``,
|
``,
|
||||||
|
`## 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-ctl spec validate ${hwpodWorkspaceArgs}\``,
|
||||||
|
`- \`hwpod inspect ${hwpodWorkspaceArgs}\``,
|
||||||
|
`- workspace reads/searches/edits: \`hwpod workspace ... ${hwpodWorkspaceArgs}\``,
|
||||||
|
`- compile check: \`hwpod build ${hwpodWorkspaceArgs}\``,
|
||||||
|
``,
|
||||||
`## Task`,
|
`## Task`,
|
||||||
agentTask.prompt,
|
agentTask.prompt,
|
||||||
``,
|
``,
|
||||||
@@ -1109,8 +1123,8 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
|
|||||||
``,
|
``,
|
||||||
`## Flow`,
|
`## Flow`,
|
||||||
`- Confirm standard \`hwpod\`, \`hwpod-ctl\` and \`hwpod-compiler\` commands are available from the gitbundle tools directory.`,
|
`- 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.`,
|
`- 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 <jobId>\` 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> ${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 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.`
|
`- CaseRun records trace/session/conversation, agent commandExecution, workspace diff and Keil build evidence without auto-grading them.`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
@@ -1367,6 +1381,10 @@ function sha256(value: string) {
|
|||||||
return createHash("sha256").update(value).digest("hex");
|
return createHash("sha256").update(value).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promptShellArg(value: string) {
|
||||||
|
return `'${String(value).replace(/'/gu, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
function sha256Buffer(value: Buffer) {
|
function sha256Buffer(value: Buffer) {
|
||||||
return createHash("sha256").update(value).digest("hex");
|
return createHash("sha256").update(value).digest("hex");
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-121
@@ -3,13 +3,11 @@ import http from "node:http";
|
|||||||
import https from "node:https";
|
import https from "node:https";
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-node-ops-contract.ts";
|
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-node-ops-contract.ts";
|
||||||
import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts";
|
import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts";
|
||||||
|
|
||||||
export const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml";
|
export const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml";
|
||||||
const HWPOD_SPEC_ENV_NAMES = ["HWPOD_SPEC_CONTENT"];
|
|
||||||
|
|
||||||
const COMPILER_NAME = "hwpod-compiler-cli";
|
const COMPILER_NAME = "hwpod-compiler-cli";
|
||||||
const CTL_NAME = "hwpod-ctl";
|
const CTL_NAME = "hwpod-ctl";
|
||||||
@@ -57,27 +55,25 @@ async function readCliStdinForCommand(argv: string[]): Promise<string | undefine
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runHwpodCompilerCli(argv: string[], options: { env?: EnvLike; now?: () => string } = {}) {
|
export async function runHwpodCompilerCli(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; now?: () => string } = {}) {
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const now = options.now ?? (() => new Date().toISOString());
|
const now = options.now ?? (() => new Date().toISOString());
|
||||||
try {
|
try {
|
||||||
const parsed = parseOptions(argv);
|
const parsed = parseOptions(argv);
|
||||||
await ensureSpecFromEnv(specPathFrom(parsed), env);
|
|
||||||
const command = parsed._[0] || "help";
|
const command = parsed._[0] || "help";
|
||||||
if (["help", "--help", "-h"].includes(command)) return result(0, compilerHelp(), now);
|
if (["help", "--help", "-h"].includes(command)) return result(0, compilerHelp(), now);
|
||||||
if (command !== "compile") throw cliError("unsupported_compiler_command", `unsupported hwpod-compiler-cli command: ${command}`);
|
if (command !== "compile") throw cliError("unsupported_compiler_command", `unsupported hwpod-compiler-cli command: ${command}`);
|
||||||
const specPath = specPathFrom(parsed);
|
|
||||||
const intent = requiredText(parsed.intent ?? parsed._[1], "intent");
|
const intent = requiredText(parsed.intent ?? parsed._[1], "intent");
|
||||||
const args = parseJsonObject(parsed.argsJson ?? parsed.args ?? "{}", "args");
|
const args = parseJsonObject(parsed.argsJson ?? parsed.args ?? "{}", "args");
|
||||||
const document = await readHwpodSpec(specPath);
|
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl: options.fetchImpl });
|
||||||
const plan = compileHwpodNodeOpsPlan({ document, specPath, intent, args, now });
|
const plan = compileHwpodNodeOpsPlan({ document: resolved.document, specPath: resolved.specPath, specAuthority: resolved.authority, intent, args, now });
|
||||||
return result(0, ok("hwpod-compiler.compile", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, plan }), now);
|
return result(0, ok("hwpod-compiler.compile", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, plan }), now);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return result(1, failure(COMPILER_NAME, error), now);
|
return result(1, failure(COMPILER_NAME, error), now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runHwpodCtl(argv: string[], options: { env?: EnvLike; now?: () => string } = {}) {
|
export async function runHwpodCtl(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; now?: () => string } = {}) {
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const now = options.now ?? (() => new Date().toISOString());
|
const now = options.now ?? (() => new Date().toISOString());
|
||||||
try {
|
try {
|
||||||
@@ -85,15 +81,13 @@ export async function runHwpodCtl(argv: string[], options: { env?: EnvLike; now?
|
|||||||
const group = parsed._[0] || "help";
|
const group = parsed._[0] || "help";
|
||||||
if (["help", "--help", "-h"].includes(group)) return result(0, ctlHelp(), now);
|
if (["help", "--help", "-h"].includes(group)) return result(0, ctlHelp(), now);
|
||||||
if (group === "bind") {
|
if (group === "bind") {
|
||||||
await ensureSpecFromEnv(specPathFrom(parsed), env);
|
|
||||||
return result(0, await bindSpec(parsed, now), now);
|
return result(0, await bindSpec(parsed, now), now);
|
||||||
}
|
}
|
||||||
if (group !== "spec") throw cliError("unsupported_ctl_command", `unsupported hwpod-ctl command: ${group}`);
|
if (group !== "spec") throw cliError("unsupported_ctl_command", `unsupported hwpod-ctl command: ${group}`);
|
||||||
const subcommand = parsed._[1] || "validate";
|
const subcommand = parsed._[1] || "validate";
|
||||||
if (subcommand === "init") return result(0, await initSpec(parsed, now), now);
|
if (subcommand === "init") return result(0, await initSpec(parsed, now), now);
|
||||||
await ensureSpecFromEnv(specPathFrom(parsed), env);
|
if (subcommand === "validate") return result(0, await validateSpec(parsed, env, options.fetchImpl), now);
|
||||||
if (subcommand === "validate") return result(0, await validateSpec(parsed), now);
|
if (subcommand === "show") return result(0, await showSpec(parsed, env, options.fetchImpl), now);
|
||||||
if (subcommand === "show") return result(0, await showSpec(parsed), now);
|
|
||||||
if (subcommand === "set") return result(0, await setSpec(parsed, now), now);
|
if (subcommand === "set") return result(0, await setSpec(parsed, now), now);
|
||||||
if (subcommand === "bind") return result(0, await bindSpec(parsed, now), now);
|
if (subcommand === "bind") return result(0, await bindSpec(parsed, now), now);
|
||||||
throw cliError("unsupported_spec_command", `unsupported hwpod-ctl spec command: ${subcommand}`);
|
throw cliError("unsupported_spec_command", `unsupported hwpod-ctl spec command: ${subcommand}`);
|
||||||
@@ -107,22 +101,20 @@ export async function runHwpodCli(argv: string[], options: { env?: EnvLike; fetc
|
|||||||
const now = options.now ?? (() => new Date().toISOString());
|
const now = options.now ?? (() => new Date().toISOString());
|
||||||
try {
|
try {
|
||||||
const parsed = parseOptions(argv);
|
const parsed = parseOptions(argv);
|
||||||
await ensureSpecFromEnv(specPathFrom(parsed), env);
|
|
||||||
const command = parsed._[0] || "help";
|
const command = parsed._[0] || "help";
|
||||||
if (["help", "--help", "-h"].includes(command)) return result(0, cliHelp(), now);
|
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 (parsed.help === true || parsed.h === true) return result(0, hwpodCliCommandHelp(command, parsed), now);
|
||||||
if (command === "closeout") return result(0, closeout(parsed), now);
|
if (command === "closeout") return result(0, closeout(parsed), now);
|
||||||
|
|
||||||
const specPath = specPathFrom(parsed);
|
|
||||||
const { intent, args } = commandToIntent(parsed, options.stdinText);
|
const { intent, args } = commandToIntent(parsed, options.stdinText);
|
||||||
const compiled = await compilePlanWithCompilerCli({ specPath, intent, args });
|
const compiled = await compilePlanWithCompilerCli({ parsed, env, fetchImpl: options.fetchImpl, intent, args });
|
||||||
const plan = compiled.plan;
|
const plan = compiled.plan;
|
||||||
if (parsed.dryRun === true) {
|
if (parsed.dryRun === true) {
|
||||||
return result(0, ok("hwpod-cli.plan", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, plan, dryRun: true }), now);
|
return result(0, ok("hwpod-cli.plan", { specPath: compiled.specPath, hwpodId: compiled.hwpodId, specAuthority: compiled.specAuthority, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, plan, dryRun: true }), now);
|
||||||
}
|
}
|
||||||
const response = await submitHwpodNodeOpsPlan({ parsed, env, fetchImpl: options.fetchImpl, plan });
|
const response = await submitHwpodNodeOpsPlan({ parsed, env, fetchImpl: options.fetchImpl, plan });
|
||||||
const exitCode = response.body?.ok === false || response.status >= 400 ? 1 : 0;
|
const exitCode = response.body?.ok === false || response.status >= 400 ? 1 : 0;
|
||||||
const payload = ok("hwpod-cli.invoke", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, route: response.route, runtimeEndpoint: response.runtimeEndpoint, body: response.body, httpStatus: response.status }, response.body?.status ?? (exitCode === 0 ? "succeeded" : "failed"));
|
const payload = ok("hwpod-cli.invoke", { specPath: compiled.specPath, hwpodId: compiled.hwpodId, specAuthority: compiled.specAuthority, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, route: response.route, runtimeEndpoint: response.runtimeEndpoint, body: response.body, httpStatus: response.status }, response.body?.status ?? (exitCode === 0 ? "succeeded" : "failed"));
|
||||||
if (exitCode !== 0) payload.ok = false;
|
if (exitCode !== 0) payload.ok = false;
|
||||||
return result(exitCode, payload, now);
|
return result(exitCode, payload, now);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -135,38 +127,7 @@ export async function readHwpodSpec(specPath = DEFAULT_HWPOD_SPEC_PATH) {
|
|||||||
return normalizeHwpodSpec(parseSimpleYaml(text, specPath), specPath);
|
return normalizeHwpodSpec(parseSimpleYaml(text, specPath), specPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSpecFromEnv(specPath = DEFAULT_HWPOD_SPEC_PATH, env: EnvLike = process.env) {
|
export function compileHwpodNodeOpsPlan({ document, specPath = DEFAULT_HWPOD_SPEC_PATH, specAuthority = "code-agent-workspace", intent, args = {}, now = () => new Date().toISOString() }: any) {
|
||||||
for (const name of HWPOD_SPEC_ENV_NAMES) {
|
|
||||||
const raw = text(env[name]);
|
|
||||||
if (!raw) continue;
|
|
||||||
try {
|
|
||||||
await readFile(specPath, "utf8");
|
|
||||||
return { source: "existing", specPath };
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.code !== "ENOENT") throw error;
|
|
||||||
}
|
|
||||||
const content = decodeHwpodSpecEnv(raw);
|
|
||||||
await mkdir(path.dirname(specPath), { recursive: true });
|
|
||||||
await writeFile(specPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
||||||
return { source: name, specPath };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeHwpodSpecEnv(value: string) {
|
|
||||||
if (/^base64:/iu.test(value)) return Buffer.from(value.replace(/^base64:/iu, ""), "base64").toString("utf8");
|
|
||||||
if (!/[\r\n]/u.test(value) && /^[A-Za-z0-9+/=_-]+$/u.test(value)) {
|
|
||||||
try {
|
|
||||||
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
||||||
if (/^apiVersion:/mu.test(decoded) && /\nkind:\s*Hwpod\b/mu.test(decoded)) return decoded;
|
|
||||||
} catch {
|
|
||||||
// Plain YAML without newlines is allowed to fall through unchanged.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compileHwpodNodeOpsPlan({ document, specPath = DEFAULT_HWPOD_SPEC_PATH, intent, args = {}, now = () => new Date().toISOString() }: any) {
|
|
||||||
const normalizedIntent = normalizeIntent(intent);
|
const normalizedIntent = normalizeIntent(intent);
|
||||||
const nodeId = document.spec.nodeBinding.nodeId;
|
const nodeId = document.spec.nodeBinding.nodeId;
|
||||||
const hwpodId = document.metadata.name || document.metadata.uid;
|
const hwpodId = document.metadata.name || document.metadata.uid;
|
||||||
@@ -183,7 +144,7 @@ export function compileHwpodNodeOpsPlan({ document, specPath = DEFAULT_HWPOD_SPE
|
|||||||
source: {
|
source: {
|
||||||
compiler: COMPILER_NAME,
|
compiler: COMPILER_NAME,
|
||||||
specPath,
|
specPath,
|
||||||
specAuthority: "code-agent-workspace"
|
specAuthority
|
||||||
},
|
},
|
||||||
resourceHints: {
|
resourceHints: {
|
||||||
workspacePath: document.spec.workspace.path,
|
workspacePath: document.spec.workspace.path,
|
||||||
@@ -200,10 +161,11 @@ function compilerHelp() {
|
|||||||
return ok("hwpod-compiler.help", {
|
return ok("hwpod-compiler.help", {
|
||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
usage: [
|
usage: [
|
||||||
|
"bun tools/hwpod-compiler-cli.ts compile --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --intent workspace.ls --args '{\"path\":\".\"}'",
|
||||||
"bun tools/hwpod-compiler-cli.ts compile --spec .hwlab/hwpod-spec.yaml --intent workspace.ls --args '{\"path\":\".\"}'",
|
"bun tools/hwpod-compiler-cli.ts compile --spec .hwlab/hwpod-spec.yaml --intent workspace.ls --args '{\"path\":\".\"}'",
|
||||||
"bun tools/hwpod-compiler-cli.ts compile --intent inspect"
|
"bun tools/hwpod-compiler-cli.ts compile --intent inspect"
|
||||||
],
|
],
|
||||||
role: "Translate workspace-local hwpod-spec plus high-level intent into hwpod-node-ops."
|
role: "Translate a runtime-resolved hwpodId plus high-level intent into hwpod-node-ops. Local --spec is an explicit debug/import path."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +173,7 @@ function ctlHelp() {
|
|||||||
return ok("hwpod-ctl.help", {
|
return ok("hwpod-ctl.help", {
|
||||||
contractVersion: "hwpod-ctl-v1",
|
contractVersion: "hwpod-ctl-v1",
|
||||||
usage: [
|
usage: [
|
||||||
|
"bun tools/hwpod-ctl.ts spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"bun tools/hwpod-ctl.ts spec init --spec .hwlab/hwpod-spec.yaml --node local-node",
|
"bun tools/hwpod-ctl.ts spec init --spec .hwlab/hwpod-spec.yaml --node local-node",
|
||||||
"bun tools/hwpod-ctl.ts spec validate --spec .hwlab/hwpod-spec.yaml",
|
"bun tools/hwpod-ctl.ts spec validate --spec .hwlab/hwpod-spec.yaml",
|
||||||
"bun tools/hwpod-ctl.ts spec set spec.workspace.path /workspace/firmware",
|
"bun tools/hwpod-ctl.ts spec set spec.workspace.path /workspace/firmware",
|
||||||
@@ -224,18 +187,18 @@ function cliHelp() {
|
|||||||
return ok("hwpod-cli.help", {
|
return ok("hwpod-cli.help", {
|
||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
usage: [
|
usage: [
|
||||||
"bun tools/hwpod-cli.ts inspect --dry-run",
|
"bun tools/hwpod-cli.ts inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts workspace ls . --dry-run",
|
"bun tools/hwpod-cli.ts workspace ls . --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts workspace cat projects/01_baseline/User/main.c --spec .hwlab/hwpod-spec.yaml",
|
"bun tools/hwpod-cli.ts workspace cat projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"bun tools/hwpod-cli.ts workspace read projects/01_baseline/User/main.c --spec .hwlab/hwpod-spec.yaml",
|
"bun tools/hwpod-cli.ts workspace read projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --spec .hwlab/hwpod-spec.yaml",
|
"bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"cat patch.txt | bun tools/hwpod-cli.ts workspace apply-patch --spec .hwlab/hwpod-spec.yaml --reason \"edit subject workspace through hwpod-node\"",
|
"cat patch.txt | bun tools/hwpod-cli.ts workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason \"edit subject workspace through hwpod-node\"",
|
||||||
"bun tools/hwpod-cli.ts workspace replace --path projects/01_baseline/User/main.c --find \"old text\" --replace \"new text\" --expected-sha <sha>",
|
"bun tools/hwpod-cli.ts workspace replace --path projects/01_baseline/User/main.c --find \"old text\" --replace \"new text\" --expected-sha <sha>",
|
||||||
"bun tools/hwpod-cli.ts workspace insert-after --path projects/01_baseline/User/main.c --anchor \"while (1)\" --line \" /* marker */\"",
|
"bun tools/hwpod-cli.ts workspace insert-after --path projects/01_baseline/User/main.c --anchor \"while (1)\" --line \" /* marker */\"",
|
||||||
"bun tools/hwpod-cli.ts build --dry-run",
|
"bun tools/hwpod-cli.ts build --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts download --dry-run",
|
"bun tools/hwpod-cli.ts download --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts job status <jobId> --dry-run",
|
"bun tools/hwpod-cli.ts job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts uart read --port uart1 --dry-run",
|
"bun tools/hwpod-cli.ts uart read --port uart1 --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
|
||||||
"bun tools/hwpod-cli.ts --api-base-url http://74.48.78.17:19667 inspect"
|
"bun tools/hwpod-cli.ts --api-base-url http://74.48.78.17:19667 inspect"
|
||||||
],
|
],
|
||||||
workspace: {
|
workspace: {
|
||||||
@@ -281,19 +244,15 @@ function hwpodWorkspaceHelp(subcommand = "") {
|
|||||||
command: "workspace",
|
command: "workspace",
|
||||||
subcommand: subcommand || null,
|
subcommand: subcommand || null,
|
||||||
usage: [
|
usage: [
|
||||||
"hwpod workspace ls [path] --spec .hwlab/hwpod-spec.yaml",
|
"hwpod workspace ls [path] --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"hwpod workspace cat <path> --spec .hwlab/hwpod-spec.yaml",
|
"hwpod workspace cat <path> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"hwpod workspace read <path> --spec .hwlab/hwpod-spec.yaml",
|
"hwpod workspace read <path> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"hwpod workspace cat --file <path> --spec .hwlab/hwpod-spec.yaml",
|
"hwpod workspace rg <pattern> [path] --context 3 --max-matches 20 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
|
||||||
"hwpod workspace cat --remote-path <path> --spec .hwlab/hwpod-spec.yaml",
|
|
||||||
"hwpod workspace rg <pattern> [path] --context 3 --max-matches 20 --spec .hwlab/hwpod-spec.yaml",
|
|
||||||
"hwpod workspace grep <pattern> [path] --spec .hwlab/hwpod-spec.yaml",
|
|
||||||
"hwpod workspace search <pattern> [path] --spec .hwlab/hwpod-spec.yaml",
|
|
||||||
"hwpod workspace write --path <path> --content <text> --expected-sha <sha>",
|
"hwpod workspace write --path <path> --content <text> --expected-sha <sha>",
|
||||||
"hwpod workspace replace --path <path> --find <text> --replace <text> --expected-sha <sha>",
|
"hwpod workspace replace --path <path> --find <text> --replace <text> --expected-sha <sha>",
|
||||||
"hwpod workspace insert-after --path <path> --anchor <text> --line <text> --expected-sha <sha>",
|
"hwpod workspace insert-after --path <path> --anchor <text> --line <text> --expected-sha <sha>",
|
||||||
"cat patch.txt | hwpod workspace apply-patch --spec .hwlab/hwpod-spec.yaml --reason <reason>",
|
"cat patch.txt | hwpod workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason <reason>",
|
||||||
"hwpod workspace apply-patch --spec .hwlab/hwpod-spec.yaml --patch-content '<apply_patch v2 envelope>'"
|
"hwpod workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --patch-content '<apply_patch v2 envelope>'"
|
||||||
],
|
],
|
||||||
aliases: {
|
aliases: {
|
||||||
read: "workspace.cat",
|
read: "workspace.cat",
|
||||||
@@ -314,7 +273,7 @@ function hwpodJobHelp(subcommand = "") {
|
|||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
command: "job",
|
command: "job",
|
||||||
subcommand: subcommand || null,
|
subcommand: subcommand || null,
|
||||||
usage: ["hwpod job status <jobId> --spec .hwlab/hwpod-spec.yaml"],
|
usage: ["hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>"],
|
||||||
boundary: "Job status compiles to cmd.run against the configured Keil CLI; it is not a separate hwpod-node op."
|
boundary: "Job status compiles to cmd.run against the configured Keil CLI; it is not a separate hwpod-node op."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -322,7 +281,7 @@ function hwpodJobHelp(subcommand = "") {
|
|||||||
function hwpodBuildHelp() {
|
function hwpodBuildHelp() {
|
||||||
return ok("hwpod-cli.build.help", {
|
return ok("hwpod-cli.build.help", {
|
||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
usage: ["hwpod build --spec .hwlab/hwpod-spec.yaml", "hwpod build --spec .hwlab/hwpod-spec.yaml --dry-run"],
|
usage: ["hwpod build --hwpod-id d601-f103-v2 --workspace-path <run-worktree>", "hwpod build --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run"],
|
||||||
boundary: "Build is a short HWPOD CLI invocation that normally starts an async Keil job through cmd.run."
|
boundary: "Build is a short HWPOD CLI invocation that normally starts an async Keil job through cmd.run."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -330,7 +289,7 @@ function hwpodBuildHelp() {
|
|||||||
function hwpodDownloadHelp() {
|
function hwpodDownloadHelp() {
|
||||||
return ok("hwpod-cli.download.help", {
|
return ok("hwpod-cli.download.help", {
|
||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
usage: ["hwpod download --spec .hwlab/hwpod-spec.yaml", "hwpod download --spec .hwlab/hwpod-spec.yaml --dry-run"],
|
usage: ["hwpod download --hwpod-id d601-f103-v2 --workspace-path <run-worktree>", "hwpod download --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run"],
|
||||||
boundary: "Download compiles to a Keil flash cmd.run plan; do not add a download-specific hwpod-node op."
|
boundary: "Download compiles to a Keil flash cmd.run plan; do not add a download-specific hwpod-node op."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -340,7 +299,7 @@ function hwpodUartHelp(subcommand = "") {
|
|||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
command: "uart",
|
command: "uart",
|
||||||
subcommand: subcommand || null,
|
subcommand: subcommand || null,
|
||||||
usage: ["hwpod uart read --spec .hwlab/hwpod-spec.yaml --port uart1"],
|
usage: ["hwpod uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --port uart1"],
|
||||||
boundary: "UART read is compiled to cmd.run against the configured serial-monitor CLI."
|
boundary: "UART read is compiled to cmd.run against the configured serial-monitor CLI."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -348,7 +307,7 @@ function hwpodUartHelp(subcommand = "") {
|
|||||||
function hwpodCmdHelp() {
|
function hwpodCmdHelp() {
|
||||||
return ok("hwpod-cli.cmd.help", {
|
return ok("hwpod-cli.cmd.help", {
|
||||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
usage: ["hwpod cmd <command> [...argv] --spec .hwlab/hwpod-spec.yaml"],
|
usage: ["hwpod cmd <command> [...argv] --hwpod-id d601-f103-v2 --workspace-path <run-worktree>"],
|
||||||
boundary: "cmd.run is the full-capability host execution op; add compiler/CLI combinations before considering new hwpod-node ops."
|
boundary: "cmd.run is the full-capability host execution op; add compiler/CLI combinations before considering new hwpod-node ops."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -369,16 +328,15 @@ async function initSpec(parsed: ParsedArgs, now: () => string) {
|
|||||||
return ok("hwpod-ctl.spec.init", { specPath, document: normalizeHwpodSpec(document, specPath) });
|
return ok("hwpod-ctl.spec.init", { specPath, document: normalizeHwpodSpec(document, specPath) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateSpec(parsed: ParsedArgs) {
|
async function validateSpec(parsed: ParsedArgs, env: EnvLike = process.env, fetchImpl?: FetchLike) {
|
||||||
const specPath = specPathFrom(parsed);
|
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
|
||||||
const document = await readHwpodSpec(specPath);
|
const document = resolved.document;
|
||||||
return ok("hwpod-ctl.spec.validate", { specPath, document, fourElements: ["targetDevice", "workspace", "debugProbe", "ioProbe"], nodeId: document.spec.nodeBinding.nodeId });
|
return ok("hwpod-ctl.spec.validate", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, document, fourElements: ["targetDevice", "workspace", "debugProbe", "ioProbe"], nodeId: document.spec.nodeBinding.nodeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showSpec(parsed: ParsedArgs) {
|
async function showSpec(parsed: ParsedArgs, env: EnvLike = process.env, fetchImpl?: FetchLike) {
|
||||||
const specPath = specPathFrom(parsed);
|
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
|
||||||
const document = await readHwpodSpec(specPath);
|
return ok("hwpod-ctl.spec.show", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, document: resolved.document });
|
||||||
return ok("hwpod-ctl.spec.show", { specPath, document });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSpec(parsed: ParsedArgs, now: () => string) {
|
async function setSpec(parsed: ParsedArgs, now: () => string) {
|
||||||
@@ -414,51 +372,74 @@ function closeout(parsed: ParsedArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compilePlanWithCompilerCli({ specPath, intent, args }: { specPath: string; intent: string; args: Record<string, unknown> }) {
|
async function compilePlanWithCompilerCli({ parsed, env, fetchImpl, intent, args }: { parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike; intent: string; args: Record<string, unknown> }) {
|
||||||
const compilerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../hwpod-compiler-cli.ts");
|
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
|
||||||
const argsJson = JSON.stringify(args ?? {});
|
const plan = compileHwpodNodeOpsPlan({ document: resolved.document, specPath: resolved.specPath, specAuthority: resolved.authority, intent, args });
|
||||||
const command = [process.execPath, compilerPath, "compile", "--spec", specPath, "--intent", intent, "--args", argsJson];
|
|
||||||
const proc = Bun.spawn(command, { cwd: process.cwd(), env: process.env, stdout: "pipe", stderr: "pipe" });
|
|
||||||
const [stdout, stderr, exitCode] = await Promise.all([
|
|
||||||
new Response(proc.stdout).text(),
|
|
||||||
new Response(proc.stderr).text(),
|
|
||||||
proc.exited
|
|
||||||
]);
|
|
||||||
const compilerInvocation = clean({
|
|
||||||
compiler: COMPILER_NAME,
|
|
||||||
mode: "subprocess",
|
|
||||||
entrypoint: "tools/hwpod-compiler-cli.ts",
|
|
||||||
argv: ["compile", "--spec", specPath, "--intent", intent, "--args", "<json>"],
|
|
||||||
exitCode,
|
|
||||||
stderr: clipText(stderr)
|
|
||||||
});
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
throw cliError("hwpod_compiler_failed", "hwpod-compiler-cli failed to compile hwpod-node-ops plan", {
|
|
||||||
compilerInvocation,
|
|
||||||
stdout: clipText(stdout),
|
|
||||||
stderr: clipText(stderr)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const payload = parseJsonObject(stdout, "compiler_stdout");
|
|
||||||
const plan = isPlainObject(payload.plan) ? payload.plan : null;
|
|
||||||
if (payload.ok === false || !plan) {
|
|
||||||
throw cliError("hwpod_compiler_invalid_output", "hwpod-compiler-cli did not return a valid hwpod-node-ops plan", {
|
|
||||||
compilerInvocation,
|
|
||||||
payload: clean({ ok: payload.ok, action: payload.action, status: payload.status, error: payload.error })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
plan,
|
plan,
|
||||||
|
specPath: resolved.specPath,
|
||||||
|
hwpodId: resolved.hwpodId,
|
||||||
|
specAuthority: resolved.authority,
|
||||||
compilerInvocation: clean({
|
compilerInvocation: clean({
|
||||||
...compilerInvocation,
|
compiler: COMPILER_NAME,
|
||||||
action: payload.action,
|
mode: "in-process",
|
||||||
contractVersion: payload.contractVersion,
|
entrypoint: "tools/src/hwpod-harness-lib.ts",
|
||||||
|
argv: ["compile", resolved.specPath.startsWith("hwpod://") ? "--hwpod-id" : "--spec", resolved.specPath.startsWith("hwpod://") ? resolved.hwpodId : resolved.specPath, "--intent", intent, "--args", "<json>"],
|
||||||
|
exitCode: 0,
|
||||||
|
action: "hwpod-compiler.compile",
|
||||||
|
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||||
planId: plan.planId,
|
planId: plan.planId,
|
||||||
source: plan.source
|
source: plan.source
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveHwpodDocument({ parsed, env, fetchImpl }: { parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike }) {
|
||||||
|
const explicitSpecPath = text(parsed.spec ?? parsed.specPath);
|
||||||
|
if (explicitSpecPath) {
|
||||||
|
const document = applyHwpodRuntimeOverrides(await readHwpodSpec(explicitSpecPath), parsed, env, explicitSpecPath);
|
||||||
|
return { document, specPath: explicitSpecPath, hwpodId: document.metadata.name || document.metadata.uid, authority: "code-agent-workspace" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hwpodId = requiredText(parsed.hwpodId ?? parsed.hwpod ?? env.HWPOD_ID ?? env.HWLAB_HWPOD_ID, "hwpodId");
|
||||||
|
const spec = await fetchHwpodSpecById({ hwpodId, parsed, env, fetchImpl });
|
||||||
|
const document = applyHwpodRuntimeOverrides(spec.document, parsed, env, `hwpod://${hwpodId}`);
|
||||||
|
return {
|
||||||
|
document,
|
||||||
|
specPath: `hwpod://${hwpodId}`,
|
||||||
|
hwpodId: document.metadata.name || document.metadata.uid || hwpodId,
|
||||||
|
authority: text(spec.authority) || text(spec.source?.kind) || "runtime-hwpod-registry"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchHwpodSpecById({ hwpodId, parsed, env, fetchImpl }: { hwpodId: string; parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike }) {
|
||||||
|
const endpoint = resolveRuntimeEndpoint({ kind: "api", parsed, env });
|
||||||
|
const route = { method: "GET", path: `/v1/hwpod/specs?hwpodId=${encodeURIComponent(hwpodId)}` };
|
||||||
|
const url = `${endpoint.baseUrl}${route.path}`;
|
||||||
|
const headers = authHeaders(parsed, env);
|
||||||
|
const response = fetchImpl
|
||||||
|
? await fetchImpl(url, { method: route.method, headers })
|
||||||
|
: await requestJsonNative(url, { method: route.method, headers, timeoutMs: numberValue(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS });
|
||||||
|
const body = await response.json().catch(() => null);
|
||||||
|
if (!body || body.ok === false) throw cliError("hwpod_registry_request_failed", "failed to query HWPOD registry", { hwpodId, httpStatus: response.status, body: compactObject(body), runtimeEndpoint: runtimeEndpointVisibility(endpoint) });
|
||||||
|
const specs = Array.isArray(body.specs) ? body.specs : [];
|
||||||
|
const match = specs.find((item: any) => text(item.hwpodId) === hwpodId || text(item.name) === hwpodId || text(item.uid) === hwpodId);
|
||||||
|
if (!match) throw cliError("hwpod_id_not_found", `HWPOD id not found in runtime registry: ${hwpodId}`, { hwpodId, count: specs.length, runtimeEndpoint: runtimeEndpointVisibility(endpoint) });
|
||||||
|
const document = objectValue(match.document);
|
||||||
|
if (!isPlainObject(document)) throw cliError("hwpod_registry_document_missing", `HWPOD registry item has no document: ${hwpodId}`, { hwpodId, item: compactObject(match) });
|
||||||
|
return { ...match, document };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHwpodRuntimeOverrides(document: any, parsed: ParsedArgs, env: EnvLike, sourcePath: string) {
|
||||||
|
const copy = JSON.parse(JSON.stringify(document));
|
||||||
|
const workspacePath = text(parsed.workspacePath ?? parsed.subjectWorktreePath ?? env.HWPOD_WORKSPACE_PATH ?? env.HWLAB_HWPOD_WORKSPACE_PATH);
|
||||||
|
if (workspacePath) {
|
||||||
|
copy.spec = objectValue(copy.spec);
|
||||||
|
copy.spec.workspace = { ...objectValue(copy.spec.workspace), path: workspacePath };
|
||||||
|
}
|
||||||
|
return normalizeHwpodSpec(copy, sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
function commandToIntent(parsed: ParsedArgs, stdinText?: string) {
|
function commandToIntent(parsed: ParsedArgs, stdinText?: string) {
|
||||||
const command = parsed._[0];
|
const command = parsed._[0];
|
||||||
if (command === "inspect") return { intent: "inspect", args: {} };
|
if (command === "inspect") return { intent: "inspect", args: {} };
|
||||||
@@ -973,6 +954,27 @@ function hwpodApiKey(parsed: ParsedArgs, env: EnvLike) {
|
|||||||
return text(env.HWLAB_API_KEY);
|
return text(env.HWLAB_API_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestJsonNative(urlValue: string, { method, headers, timeoutMs }: { method: string; headers: Record<string, string>; timeoutMs: number }) {
|
||||||
|
return new Promise<any>((resolve, reject) => {
|
||||||
|
const url = new URL(urlValue);
|
||||||
|
const client = url.protocol === "https:" ? https : http;
|
||||||
|
const request = client.request(url, { method, headers, timeout: timeoutMs }, (response) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
response.on("end", () => {
|
||||||
|
const textValue = Buffer.concat(chunks).toString("utf8");
|
||||||
|
resolve({
|
||||||
|
status: response.statusCode ?? 0,
|
||||||
|
json: async () => textValue ? JSON.parse(textValue) : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
request.on("timeout", () => request.destroy(cliError("hwpod_registry_timeout", `HWPOD registry request timed out after ${timeoutMs}ms`)));
|
||||||
|
request.on("error", reject);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function postJsonNative(urlValue: string, { method, headers, body, timeoutMs }: { method: string; headers: Record<string, string>; body: string; timeoutMs: number }) {
|
function postJsonNative(urlValue: string, { method, headers, body, timeoutMs }: { method: string; headers: Record<string, string>; body: string; timeoutMs: number }) {
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
const url = new URL(urlValue);
|
const url = new URL(urlValue);
|
||||||
@@ -1223,6 +1225,16 @@ function clean<T extends Record<string, unknown>>(value: T): T {
|
|||||||
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T;
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactObject(value: any) {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
if (!isPlainObject(value) && !Array.isArray(value)) return value;
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
return { value: String(value) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cleanTextEditArgs<T extends Record<string, unknown>>(value: T): T {
|
function cleanTextEditArgs<T extends Record<string, unknown>>(value: T): T {
|
||||||
return Object.fromEntries(Object.entries(value).filter(([key, item]) => item !== undefined && item !== null && (item !== "" || ["replace", "line"].includes(key)))) as T;
|
return Object.fromEntries(Object.entries(value).filter(([key, item]) => item !== undefined && item !== null && (item !== "" || ["replace", "line"].includes(key)))) as T;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user