diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index c16477a6..9d3ba6ab 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -21,15 +21,15 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H - Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutation;Web reducer/selectors、route hydrate、GET/list/detail/messages/SSE consumer、fake-server 和测试 helper 都不能由读侧失败、404、空列表、网络瞬断或 late response 触发这些 mutation。只有用户显式 mutation 成功,或后端 canonical lifecycle projection,能改变 session lifecycle。需求真相见 UniDesk OA `PJ2026-010401 Web工作台` 的“破坏性投影权”。 - fake-server 不是第二后端:它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*`、`/v1/*`、`/health*` 请求应失败并暴露 path;不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。 - 真实数据优先:fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界,并标明 `derivedFrom` 与 `syntheticReason`。 -- 原入口闭环:fake-server Playwright 用例负责可重复红灯和源码回归;线上 `hwlab nodes web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。 +- 原入口闭环:fake-server Playwright 用例负责可重复红灯和源码回归;线上 `web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。 - Master server 禁重型验证:不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。 -- 禁止裸写 Playwright:UniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `hwlab nodes web-probe run|script|observe`;不得直接写 `trans playwright` heredoc、`playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有非 HWLAB 外站截图/PDF 等 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。 +- 禁止裸写 Playwright:UniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`;不得直接写 `trans playwright` heredoc、`playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有非 HWLAB 外站截图/PDF 等 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。 ## 工作流 1. 定位目标:确认 repo、node、lane、workspace、public origin 和目标 SPEC。HWLAB D601 v0.3 例子是 `/home/ubuntu/workspace/hwlab-v03` + `https://hwlab.pikapython.com`,但只能在 issue/CLI 指向该目标时使用。 2. 读规则:进入目标 workspace 前读取目标 `AGENTS.md`;涉及规格或测试设计时读取 UniDesk OA 对应 SPEC。 -3. 线上复现:短动作用 `hwlab nodes web-probe run|script`,长程 Workbench/session 观测用 `hwlab nodes web-probe observe start|command|status|stop|analyze`;保存 `scriptSha256`、`runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id。 +3. 线上复现:短动作用 `web-probe run|script`,长程 Workbench/session 观测用 `web-probe observe start|command|status|stop|analyze`;保存 `scriptSha256`、`runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id。 4. 建红灯:修 Workbench/Performance 用户可见 bug 前,在目标 HWLAB workspace 的 `web/hwlab-cloud-web` fake-server Playwright 套件中补确定性用例;fixture 从真实采集样本脱敏产生。 5. 修源码:保持状态读写单一路径。状态投影类修复优先收敛 server-state/reducer/projection,不在 UI 组件、trace polling、result polling 或 localStorage 中新增竞争事实。 6. 验证:先跑 fake-server Playwright 目标用例,再回到同一 node/lane public origin 用 web-probe 复测;多轮任务必须用同一个 observer/session 采样到终态。截图、report hash 和 analyze finding 作为证据回传,不进入源码仓库。 @@ -40,7 +40,7 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H 线上 Cloud Web DOM 验收优先使用受控入口: ```bash -bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 +bun scripts/cli.ts web-probe run --node D601 --lane v03 ``` web-probe 入口分三类: @@ -54,7 +54,7 @@ web-probe 入口分三类: 需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright: ```bash -bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS' +bun scripts/cli.ts web-probe script --node D601 --lane v03 <<'JS' export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => { await gotoStable('/workbench'); const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5'); @@ -82,27 +82,27 @@ JS 长程 Workbench 观测使用 `observe` 命令组: ```bash -bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55 -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type newSession -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF' +bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55 +bun scripts/cli.ts web-probe observe command webobs-xxxx --type newSession +bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectProvider --provider codex-api +bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type cancel +bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF' long prompt EOF -bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx --tail-lines 6 -bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view turn-summary -bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42 -bun scripts/cli.ts hwlab nodes web-probe observe stop webobs-xxxx -bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx -bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node D601 --lane v03 --dry-run -bun scripts/cli.ts hwlab nodes web-probe sentinel status --node D601 --lane v03 -bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node D601 --lane v03 -bun scripts/cli.ts hwlab nodes web-probe sentinel image build --node D601 --lane v03 --dry-run -bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run -bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node D601 --lane v03 -bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run +bun scripts/cli.ts web-probe observe status webobs-xxxx --tail-lines 6 +bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary +bun scripts/cli.ts web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42 +bun scripts/cli.ts web-probe observe stop webobs-xxxx +bun scripts/cli.ts web-probe observe analyze webobs-xxxx +bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run +bun scripts/cli.ts web-probe sentinel status --node D601 --lane v03 +bun scripts/cli.ts web-probe sentinel image status --node D601 --lane v03 +bun scripts/cli.ts web-probe sentinel image build --node D601 --lane v03 --dry-run +bun scripts/cli.ts web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run +bun scripts/cli.ts web-probe sentinel control-plane status --node D601 --lane v03 +bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .state/web-probe-sentinel-smoke --scheduler-disabled --once ``` @@ -111,23 +111,23 @@ bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .s 项目管理 / MDTODO 页面同样优先使用 `observe`,不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`,同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch: ```bash -bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55 -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type gotoProjectMdtodo -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/ -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type probeMdtodoSource -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type reindexMdtodoSource -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task --text 'web-probe interactive edit acceptance' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task --text 'body updated through web-probe command' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task --status completed -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent --title 'web-probe command subtask' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type continueMdtodoTask --task --title 'web-probe sibling task' -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type deleteMdtodoTask --task -bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task -bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view project-mdtodo-summary -bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx +bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55 +bun scripts/cli.ts web-probe observe command webobs-xxxx --type gotoProjectMdtodo +bun scripts/cli.ts web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/ +bun scripts/cli.ts web-probe observe command webobs-xxxx --type probeMdtodoSource +bun scripts/cli.ts web-probe observe command webobs-xxxx --type reindexMdtodoSource +bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id +bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref +bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref +bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task --text 'web-probe interactive edit acceptance' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task --text 'body updated through web-probe command' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task --status completed +bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent --title 'web-probe command subtask' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task --title 'web-probe sibling task' +bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task +bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task +bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary +bun scripts/cli.ts web-probe observe analyze webobs-xxxx ``` `observe command --type newSession` 是显式用户/control action:它通过当前 Workbench UI 点击 `#session-create` 创建新 session,等待 active session id 改变和 composer ready,并把前后 snapshot 写入 control log。它只能用于采样开始时建立目标 session,或作为用户明确的新建会话动作;不得在 route/session mismatch 后当作 repair 手段。 @@ -253,7 +253,7 @@ UniDesk 主前端交付门禁仍由 `bun scripts/cli.ts e2e run` 承担;本技 ## 受控 web-probe 代替裸 Playwright -UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时,统一选择 `hwlab nodes web-probe run|script|observe`。 +UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时,统一选择 `web-probe run|script|observe`。 `web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告:可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称;点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。 diff --git a/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md b/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md index 6dce1207..7794b477 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md +++ b/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md @@ -264,7 +264,7 @@ rolling recovery 相关 span 应能用 OTel trace id/request id 关联 `sessionI | --- | --- | --- | --- | | OPS-MON-REQ-008 | Web哨兵 | [PJ2026-01060508 Web哨兵](PJ2026-01060508-web-probe-sentinel.md) | [Web工作台](PJ2026-010401-web-workbench.md)、[Workbench唯一投影](PJ2026-0104010803-workbench-unique-projection.md)、[YAML运维](PJ2026-010603-yaml-first-ops.md)、[公开入口](PJ2026-010604-public-entry.md)、[发布流水](PJ2026-010601-controlled-release.md) | -运维监控应提供 YAML-first Web 哨兵能力,把现有 `hwlab nodes web-probe observe start/status/command/collect/analyze` 服务化为生产可用的持续 canary、报告视图和发布恢复验证入口。该能力只 wrap 现有 observe runner、control queue、artifact JSONL、`collect` 渲染和 `observe analyze` 报告,不新增第二套 Playwright runner、offline analyzer、finding classifier、dashboard 事实源或 Workbench 状态机。 +运维监控应提供 YAML-first Web 哨兵能力,把现有 `web-probe observe start/status/command/collect/analyze` 服务化为生产可用的持续 canary、报告视图和发布恢复验证入口。该能力只 wrap 现有 observe runner、control queue、artifact JSONL、`collect` 渲染和 `observe analyze` 报告,不新增第二套 Playwright runner、offline analyzer、finding classifier、dashboard 事实源或 Workbench 状态机。 Web 哨兵配置必须通过 node/lane root YAML 的 `observability.webProbe.sentinel.enabled/configRefs` 引用 runtime、scenario、promptSet、reportView、publicExposure、CI/CD 和 Secret owning YAML。parser 只校验引用、形状、类型和冲突;cadence、采样间隔、轮次、profile、prompt source、report view、retention、public exposure、maintenance 和 Secret 参数以 YAML 为准。 diff --git a/scripts/cli.ts b/scripts/cli.ts index 1db70e7c..f26254c4 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -316,6 +316,20 @@ async function main(): Promise { return; } + if (top === "web-probe") { + const { runWebProbeCommand } = await import("./src/web-probe"); + const result = await runWebProbeCommand(readConfig(), args.slice(1)); + const ok = (result as { ok?: unknown }).ok !== false; + if (isRenderedCliResult(result)) { + emitText(result.renderedText, result.command || commandName); + if (!ok) process.exitCode = 1; + return; + } + emitJson(commandName, result, ok); + if (!ok) process.exitCode = 1; + return; + } + if (top === "hwlab") { if (sub === "node" || sub === "nodes") { const { runHwlabNodeCommand } = await import("./src/hwlab-node"); diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 79293e24..49b4fe43 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -59,7 +59,8 @@ export function rootHelp(): unknown { { command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, issue/comment apply_patch body patching, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." }, { command: "git github-push-fallback [--repo owner/name] [--branch branch] [--host-name host-or-ip] [--confirm]", description: "Plan or execute a one-shot GitHub push through ssh.github.com:443 without editing remotes; use only for reviewed DNS/port-22 push fallback." }, { command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run approval preview without live bridges or message sends." }, - { command: "hwlab nodes control-plane|git-mirror|secret|test-accounts|web-probe --node --lane ", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared k3s infra/tools-image/Argo bootstrap, redacted test-account preparation, Web DOM probe credential injection, and G14 v0.3+ runtime lanes, with the node identity passed as data." }, + { command: "web-probe run|script|observe|sentinel --node --lane ", description: "Run YAML-selected HWLAB Web probes, long observe/analyze sessions, project-management MDTODO commands, and Web sentinel control through the single top-level web-probe entrypoint." }, + { command: "hwlab nodes control-plane|git-mirror|secret|test-accounts --node --lane ", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared k3s infra/tools-image/Argo bootstrap, redacted test-account preparation, and G14 v0.3+ runtime lanes, with the node identity passed as data." }, { command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." }, { command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." }, { command: "platform-infra sub2api|langbot|n8n|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot and n8n, manage YAML-controlled public FRP/Caddy exposure and WeChat archive workflows, and inspect status/logs without printing secrets." }, @@ -662,7 +663,7 @@ function platformInfraHelpSummary(): unknown { function hwlabNodeHelpSummary(): unknown { return { - command: "hwlab nodes control-plane|git-mirror|observability|secret|test-accounts|web-probe --node --lane ", + command: "hwlab nodes control-plane|git-mirror|observability|secret|test-accounts --node --lane ", output: "json", usage: [ "bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03", @@ -675,9 +676,22 @@ function hwlabNodeHelpSummary(): unknown { "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name ", "bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000", ], - description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; observability reads runtime metrics and authenticated Web Performance summaries; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output; web-probe runs the target workspace DOM probe with bootstrap Web credentials injected only as one-shot stdin/env.", + description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; observability reads runtime metrics and authenticated Web Performance summaries; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output. Web probe commands moved to top-level `bun scripts/cli.ts web-probe`.", + }; +} + +function webProbeHelpSummary(): unknown { + return { + command: "web-probe run|script|observe|sentinel --node --lane ", + output: "json", + usage: [ + "bun scripts/cli.ts web-probe run --node D601 --lane v03 --wait-messages-ms 1000", + "bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000", + "bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary", + "bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run", + ], + description: "Run target node/lane HWLAB Cloud Web probes, long observe/analyze sessions, project-management MDTODO commands, and Web sentinel YAML-first control through a single top-level implementation.", }; } @@ -749,15 +763,14 @@ export async function staticNamespaceHelp(args: string[]): Promise (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary()); if (top === "platform-db") return platformDbHelp(); if (top === "secrets") return secretsHelp(); + if (top === "web-probe") return loadHelp(async () => (await import("./web-probe")).webProbeHelp(), webProbeHelpSummary()); + if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "web-probe") return null; if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "control-plane" && args[3] === "infra") { return loadHelp(async () => (await import("./hwlab-node-control-plane")).hwlabNodeControlPlaneInfraHelp(), hwlabNodeHelpSummary()); } if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "test-accounts") { return loadHelp(async () => (await import("./hwlab-test-accounts")).hwlabTestAccountsHelp(), hwlabNodeHelpSummary()); } - if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "web-probe") { - return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeWebProbeHelp(), hwlabNodeHelpSummary()); - } if (top === "hwlab" && (sub === "node" || sub === "nodes")) return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeHelp(), hwlabNodeHelpSummary()); if (top === "hwlab" && sub === "g14") return loadHelp(async () => (await import("./hwlab-g14")).hwlabG14Help(), hwlabG14HelpSummary()); if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary()); diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index 9e04f329..6a4d037a 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -1,80 +1,33 @@ // SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. -// Responsibility: Help payloads for the HWLAB node/lane CLI. +// Responsibility: Help payloads for HWLAB node/lane and web-probe CLI entries. import { hwlabRuntimeLaneConfigPath } from "./hwlab-node-lanes"; export function hwlabNodeHelp(): Record { return { ok: true, command: "hwlab nodes", - description: "Node/lane oriented HWLAB operations. G14 is a node id value passed by --node, not a command family.", + description: "Node/lane oriented HWLAB runtime operations. Web probes moved to the top-level web-probe command.", configPath: hwlabRuntimeLaneConfigPath(), examples: [ "bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane infra status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes control-plane infra tools-image status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane infra tools-image build --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane infra argo status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane plan --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes control-plane status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane status --node D601 --lane v03 --full", - "bun scripts/cli.ts hwlab nodes control-plane cleanup-runs --node D601 --lane v03 --min-age-minutes 60 --limit 20 --dry-run", - "bun scripts/cli.ts hwlab nodes control-plane cleanup-runs --node D601 --lane v03 --min-age-minutes 60 --limit 20 --confirm --wait", - "bun scripts/cli.ts hwlab nodes control-plane status --node G14 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane apply --node G14 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes control-plane refresh --node G14 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane runtime-image status --node G14 --lane v03", - "bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node G14 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane sync --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane public-exposure --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes control-plane public-exposure --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane trigger-current --node G14 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane trigger-current --node D601 --lane v03 --confirm --wait", - "bun scripts/cli.ts hwlab nodes control-plane trigger-current --node D601 --lane v03 --confirm --wait --rerun", - "bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes control-plane allow-endpoint-bridge --node G14 --lane v03 --confirm", "bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03", - "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-openfga", - "bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key --confirm", - "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-bootstrap-admin", - "bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-bootstrap-admin --confirm", - "bun scripts/cli.ts hwlab nodes secret ensure --node D601 --lane v03 --name hwlab-v03-bootstrap-admin --confirm --force", - "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-cloud-api-v03-db", - "bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes secret cleanup-obsolete --node G14 --lane v03 --name hwpod-v03-db --dry-run", - "bun scripts/cli.ts hwlab nodes secret cleanup-obsolete --node G14 --lane v03 --name hwpod-v03-db --confirm", - "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-code-agent-provider", - "bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm", + "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name ", "bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm", - "bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000", - "bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel image build --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait", - "bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --latest --view turn-summary", - "bun scripts/cli.ts hwlab nodes observability plan --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes observability status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes observability apply --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes observability workbench-summary --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes observability performance-summary --node D601 --lane v03", + "bun scripts/cli.ts web-probe --help", ], + actions: { + "control-plane": "YAML-first node-local CI/CD, git-mirror, public exposure, runtime-image, Argo and PipelineRun operations.", + "git-mirror": "Inspect or operate the selected node/lane source mirror.", + secret: "Inspect and sync YAML-declared runtime Secrets without printing secret values.", + "test-accounts": "Prepare YAML-declared HWLAB admin/test account API keys with redacted sourceRef/fingerprint output.", + observability: "Read runtime metrics and authenticated Web Performance summaries.", + }, notes: [ - "`trigger-current` skips an already succeeded PipelineRun for the same HWLAB source commit by default.", - "`trigger-current --confirm --wait` is the one-command CICD path: git-mirror pre-sync/pre-flush, control-plane refresh, PipelineRun create/reuse, bounded wait, and post-flush when terminal.", - "`control-plane sync --confirm` syncs the YAML-declared local-k3s postgres bootstrap Secret, terminates a stale running Argo operation, deletes failed Argo hook Jobs, and recreates stale non-ready StatefulSet pods that are still pinned to an old controller revision with pull/backoff errors.", - "`--wait` defaults to 120 seconds. If the PipelineRun is still active after 120 seconds, the CLI returns a warning plus env-reuse and git-mirror inspection commands instead of blocking.", - "`web-probe sentinel image/control-plane` renders the YAML-first image, GitOps and Argo plan from the owning configRefs; unavailable service validation is a structured failure, not an automatic second execution path.", - "`web-probe sentinel validate|maintenance|report` talks to the sentinel through k3s internal Service DNS, records only analyze summaries/views, and never runs a public/fallback validation path.", - "Use `--rerun` for a deliberate YAML-first config-only publish when UniDesk node/lane render inputs changed but the HWLAB source commit did not." + "`web-probe` is no longer under `hwlab nodes`; use `bun scripts/cli.ts web-probe ...`.", + "`--node` and `--lane` remain data arguments resolved from YAML for node/lane operations.", + "`trigger-current --confirm --wait` is the one-command CICD path for current node/lane runtime publish.", ], }; } @@ -82,73 +35,36 @@ export function hwlabNodeHelp(): Record { export function hwlabNodeWebProbeHelp(): Record { return { ok: true, - command: "hwlab nodes web-probe", - description: "Run target node/lane HWLAB Cloud Web DOM probes with Web login credentials resolved from YAML-declared bootstrap admin sourceRef and injected only as one-shot stdin/env.", + command: "web-probe", + description: "Run target node/lane HWLAB Cloud Web probes with YAML-selected origin and one-shot bootstrap Web credentials.", examples: [ - "bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000", - "bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --fresh-session --message 'ping'", - "bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs", - "bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000", - "bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type newSession", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'\nlong prompt\nEOF", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type gotoProjectMdtodo", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --workspace-root 'F:\\\\Work\\\\HWLAB-CASE-F103' --root docs/MDTODO", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref docs/MDTODO/example.md", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoTask --task R1.1", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task R1.1 --title '更新任务标题'", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type addMdtodoSubTask --task R1.1 --title '新增子任务'", - "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task R1.1", - "bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx", - "bun scripts/cli.ts hwlab nodes web-probe observe stop webobs-xxxx --force", - "bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view turn-summary", - "bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42", - "bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view project-mdtodo-summary", - "bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx", - "bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel image build --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run", - "bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03", - "bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03 --quick-verify --confirm --wait", - "bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance start --node D601 --lane v03 --confirm --wait --release-id ", - "bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait --release-id ", - "bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --latest --view summary", - "bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --latest --view trace-frame", - "bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const apiMatrix = await fetchApiMatrix(['/v1/workbench/workspace?projectId=prj_hwpod_workbench', '/auth/session']);\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n recordStep('workbench-summary', { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, apiMatrixOk: apiMatrix.ok });\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS", + "bun scripts/cli.ts web-probe run --node D601 --lane v03 --wait-messages-ms 1000", + "bun scripts/cli.ts web-probe run --node D601 --lane v03 --fresh-session --message 'ping'", + "bun scripts/cli.ts web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs", + "bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000", + "bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000", + "bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'", + "bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task R1.1", + "bun scripts/cli.ts web-probe observe status webobs-xxxx", + "bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary", + "bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary", + "bun scripts/cli.ts web-probe observe analyze webobs-xxxx", + "bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run", + "bun scripts/cli.ts web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait --release-id ", ], actions: { run: "Run the repo-owned scripts/web-live-dom-probe.mjs helper.", - script: "Run caller-provided Playwright JS after CLI-managed /auth/login; the script receives authenticated browser/context/page plus gotoStable/reloadStable/gotoCurrentStable/safeReload/fetchJson/safeFetchJson/fetchApiMatrix/recordStep/collectText/safeEvaluate/waitWorkbenchReady/screenshotOnError/summarizeWorkspace/summarizeConversation helpers and must not handle secrets itself.", - observe: "Start, inspect, control, stop, collect, and analyze a pure-client long-running Workbench observer on the target host. The observer runs a control page plus a passive observer page in a shared-auth browser context, receives commands through stateDir/commands files, writes JSONL artifacts, and does not expose any inbound service API.", - sentinel: "Render the YAML-first service wrapper configRef graph plus image/GitOps/Argo control-plane plan for the production web-probe sentinel, then validate/maintenance/report through the k3s internal sentinel Service DNS. Quick verify still uses web-probe observe/analyze artifacts as truth and records only redacted report summaries/views.", + script: "Run caller-provided Playwright JS after CLI-managed /auth/login; scripts must not handle secrets themselves.", + observe: "Start, inspect, control, stop, collect, and analyze a long-running observer that writes JSONL artifacts.", + sentinel: "Render and operate the YAML-first web-probe sentinel wrapper, image, GitOps, maintenance and report views.", }, notes: [ - "The default probe URL, browser proxy mode, observe/analyze alert thresholds, and project-management observe behavior come from config/hwlab-node-lanes.yaml webProbe; pass --url only when intentionally overriding the YAML-selected origin.", - "Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.", - "Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.", - "observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.", - "observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze instead of repeating --node/--lane/--state-dir.", - "observe status reports runner liveness separately from process existence, including heartbeat age, stale threshold, command backlog, and abandoned command counts.", - "observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/steer/cancel/goto/screenshot/mark/stop. Project-management actions include gotoProjectMdtodo, source config/probe/reindex, selectMdtodoSource/selectMdtodoFile/selectMdtodoTask/expandMdtodoTask, edit/toggle/add/continue/delete MDTODO task commands, and launchWorkbenchFromMdtodo. These actions are allowed only by the selected node/lane YAML. steer/cancel reuse the Workbench composer path, and project-management launch uses public UI/API evidence plus x-hwlab-otel-trace-id capture, not private backend APIs. For long prompts and task body edits use --text-stdin; keep prompt/body text out of issue comments by citing textHash/textBytes.", - "observe stop --force first checks heartbeat/backlog health; if the runner is stale or commands are not being consumed, it kills the recorded PID from outside the command queue and marks pending/processing commands abandoned so analyze classifies them as tooling findings.", - "observe collect --view turn-summary renders the multi-turn CLI reading layer from samples/control artifacts; --view trace-frame renders one sampled trace frame with a fixed Final Response block; --view project-summary is the legacy project view, and --view project-mdtodo-summary renders MDTODO samples, interactive command/mutation rows, Workbench launch commands, and OTel trace drill-down commands from the same artifacts. collect views do not save a second source of truth.", - "observe analyze is offline-only: it reads artifact JSONL plus observer command/heartbeat artifacts and writes analysis/report.md plus analysis/report.json without accessing Workbench APIs or driving the browser. For project-management pages it reports DOM readiness, public task id coverage, Workbench launch success/failure, captured launch OTel trace headers, and YAML-budgeted project API timing.", - "observe analyze scans every sampled DOM point, extracts Workbench timing text such as 总耗时/total and 最近 N 秒/分前, and writes a sample point vs turn timing report: each Markdown table row starts with the timestamp, followed by each turn's 总耗时(s) and 最近更新(s). Timing series are reported for post-processing/manual analysis instead of auto-judged from status tail output.", - "observe analyze also reports visible “加载中” count, owner attribution, concurrent loading owners, and continuous visible segments; fixes must reduce real loading latency, not reveal incomplete content early to make this metric disappear.", - "script/observe support --browser-proxy-mode auto|direct; use direct for A/B evidence when frontend RUM is slow but OTel server spans are absent or fast, instead of falling back to raw Playwright.", - "sentinel plan/status is a configuration visibility command for the service wrapper; sentinel image/control-plane renders image, GitOps and Argo control-plane state from owning YAML. sentinel validate checks /api/health, /metrics, recent analyze report and publicExposure; maintenance stop can run the configured quick verify and index the observe/analyze report. observe start/status/command/collect/analyze remain the sampling and analysis truth.", - "Use recordStep(name, data) or fetchApiMatrix(paths) to keep structured partial evidence when a later step fails.", - "Use reloadStable(), gotoCurrentStable(), or safeReload() for bounded retries around page reload/current-URL navigation jitter such as ERR_NETWORK_CHANGED.", - "Playwright page.evaluate accepts one serializable argument; use page.evaluate(({ a, b }) => ..., { a, b }) or safeEvaluate(fn, { a, b }).", - "Failures include failureKind, errorMessage, scriptSha256, runDir, lastUrl, and lastScreenshot when a screenshot can be captured.", + "Default URL, browser proxy mode, observe/analyze thresholds, and project-management command allowlist come from config/hwlab-node-lanes.yaml webProbe.", + "observe is passive by default; user actions must be explicit observe command entries in control.jsonl.", + "After observe start, prefer observe status|command|stop|collect|analyze instead of repeating --node/--lane/--state-dir.", + "collect views render bounded summaries from existing artifacts and do not create a second source of truth.", + "analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json.", + "Issue evidence should cite observer id, stateDir, report SHA, screenshot SHA, command ids and concise summaries, not prompt/provider/secret payloads.", ], }; } diff --git a/scripts/src/hwlab-node-web-observe-collect.ts b/scripts/src/hwlab-node-web-observe-collect.ts index c6be4a05..e763cc8b 100644 --- a/scripts/src/hwlab-node-web-observe-collect.ts +++ b/scripts/src/hwlab-node-web-observe-collect.ts @@ -299,7 +299,7 @@ function renderTurnSummary(rows){ const title='Workbench Session '+anchorSessionId+' observer '+(manifest.jobId||'-')+driftSuffix; const header='轮次 用户消息 Trace 状态 耗时/最近 标记 Final Response'; const body=rows.map((row)=>pad(row.round,5)+' '+pad((row.userHash?row.userHash.slice(0,18)+' ':'')+row.userPreview,32)+' '+pad(row.traceId||'-',12)+' '+pad(row.status,10)+' '+pad(fmtDuration(row.elapsedSeconds)+'/'+(row.recentUpdateSeconds===null?'-':String(row.recentUpdateSeconds)+'s'),13)+' '+pad(row.marks,11)+' '+short(row.finalResponse?.preview||'(空内容)',80)).join('\\n'); - return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts hwlab nodes web-probe observe collect '+(manifest.jobId||'')+' --view trace-frame --trace-id --sample-seq '].join('\\n'); + return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts web-probe observe collect '+(manifest.jobId||'')+' --view trace-frame --trace-id --sample-seq '].join('\\n'); } function diagnosticRows(label,items,limit=6){ const selected=items.slice(-limit); diff --git a/scripts/src/hwlab-node-web-observe-render.ts b/scripts/src/hwlab-node-web-observe-render.ts index 20d3cc62..c3855a54 100644 --- a/scripts/src/hwlab-node-web-observe-render.ts +++ b/scripts/src/hwlab-node-web-observe-render.ts @@ -1,13 +1,13 @@ // SPEC: PJ2026-01040111 long-running Workbench observation. // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. -// Responsibility: CLI text rendering for hwlab nodes web-probe observe status/command/collect. +// Responsibility: CLI text rendering for web-probe observe status/command/collect. import type { RenderedCliResult } from "./output"; import { renderWebObserveWrapperContract } from "./hwlab-node-web-observe-wrapper-render"; export function withWebObserveStatusRendered(value: Record): RenderedCliResult { return { ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe status", + command: typeof value.command === "string" ? value.command : "web-probe observe status", contentType: "text/plain", renderedText: renderWebObserveStatusTable(value), }; @@ -16,7 +16,7 @@ export function withWebObserveStatusRendered(value: Record): Re export function withWebObserveCommandRendered(value: Record): RenderedCliResult { return { ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe command", + command: typeof value.command === "string" ? value.command : "web-probe observe command", contentType: "text/plain", renderedText: renderWebObserveCommandTable(value), }; @@ -25,7 +25,7 @@ export function withWebObserveCommandRendered(value: Record): R export function withWebObserveCollectRendered(value: Record): RenderedCliResult { return { ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe collect", + command: typeof value.command === "string" ? value.command : "web-probe observe collect", contentType: "text/plain", renderedText: renderWebObserveCollectTable(value), }; @@ -63,7 +63,7 @@ function renderWebObserveStatusTable(value: Record): string { const heartbeatStale = diagnostics?.heartbeatStale ?? heartbeat?.stale; const heartbeatLiveness = diagnostics?.effectiveLiveness ?? heartbeat?.effectiveLiveness; const lines = [ - `hwlab nodes web-probe observe status (${webObserveText(value.status)})`, + `web-probe observe status (${webObserveText(value.status)})`, "", ...renderWebObserveWrapperContract(value), webObserveTable(["ID", "NODE", "LANE", "LIVENESS", "ALIVE", "PID", "SAMPLE", "CMD_SEQ", "HB_AGE_S", "STALE", "UPDATED", "TARGET"], [[ @@ -190,7 +190,7 @@ function renderWebObserveCommandTable(value: Record): string { const id = webObserveText(value.id); const full = value.full === true; const lines = [ - `hwlab nodes web-probe observe command (${webObserveText(value.status)})`, + `web-probe observe command (${webObserveText(value.status)})`, "", ...renderWebObserveWrapperContract(value), webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[ @@ -211,8 +211,8 @@ function renderWebObserveCommandTable(value: Record): string { ] : []), "", "Next:", - ` bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - ` bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, + ` bun scripts/cli.ts web-probe observe status ${id}`, + ` bun scripts/cli.ts web-probe observe analyze ${id}`, "", "Disclosure:", " default view is a bounded command result; pass --full to expose the complete command result JSON.", @@ -225,7 +225,7 @@ function renderWebObserveCollectTable(value: Record): string { const collect = nullableRecord(value.collect); if (typeof collect?.renderedText === "string") { return [ - `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, + `web-probe observe collect (${webObserveText(value.status)})`, "", ...renderWebObserveWrapperContract(value), collect.renderedText, @@ -261,7 +261,7 @@ function renderWebObserveCollectTable(value: Record): string { const jsonFindingDetail = record(jsonSummary.findingDetail); const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record => item !== null).slice(0, 12) : []; const lines = [ - `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, + `web-probe observe collect (${webObserveText(value.status)})`, "", ...renderWebObserveWrapperContract(value), webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[ @@ -467,7 +467,7 @@ function renderWebObserveProjectCollectTable(value: Record, col const launches = Array.isArray(collect.launches) ? collect.launches.map(record).filter((item): item is Record => item !== null).slice(-4) : []; const findings = Array.isArray(collect.findings) ? collect.findings.map(record).filter((item): item is Record => item !== null).slice(0, 6) : []; const lines = [ - `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, + `web-probe observe collect (${webObserveText(value.status)})`, "", ...renderWebObserveWrapperContract(value), webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[ diff --git a/scripts/src/hwlab-node-web-observe-wrapper.ts b/scripts/src/hwlab-node-web-observe-wrapper.ts index 3376d952..d7ccda8b 100644 --- a/scripts/src/hwlab-node-web-observe-wrapper.ts +++ b/scripts/src/hwlab-node-web-observe-wrapper.ts @@ -1,5 +1,5 @@ // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. -// Responsibility: Stable wrapper contract for the existing hwlab nodes web-probe observe CLI verbs. +// Responsibility: Stable wrapper contract for the existing web-probe observe CLI verbs. export type WebObserveWrapperAction = "start" | "status" | "command" | "stop" | "collect" | "analyze"; @@ -92,7 +92,7 @@ export function buildWebObserveWrapperContract(input: WebObserveWrapperInput): R id: observerId, jobId: input.jobId ?? observerId, cli: { - family: "bun scripts/cli.ts hwlab nodes web-probe observe", + family: "bun scripts/cli.ts web-probe observe", commandShape: webObserveWrapperCommandShape(input), verb: input.action, valuesRedacted: true, @@ -185,7 +185,7 @@ function artifactPath(stateDir: string | null, relative: string): string | null } function webObserveWrapperCommandShape(input: WebObserveWrapperInput): string { - const base = ["bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", input.action]; + const base = ["bun", "scripts/cli.ts", "web-probe", "observe", input.action]; const observerId = input.id ?? input.jobId; const parts = observerId === null ? [...base, "--node", input.node, "--lane", input.lane] diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 4d160551..40df9476 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -172,7 +172,7 @@ export function runWebProbeSentinelCommand(spec: HwlabRuntimeLaneSpec, options: } function runSentinelImage(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = `hwlab nodes web-probe sentinel image ${options.action}`; + const command = `web-probe sentinel image ${options.action}`; if (options.action === "build" && options.confirm) { if (!options.wait) return renderAsyncSentinelJob(state, "image", "build", options.timeoutSeconds); return runSentinelImageBuildConfirmed(state, options); @@ -192,10 +192,10 @@ function runSentinelImage(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = `hwlab nodes web-probe sentinel control-plane ${options.action}`; + const command = `web-probe sentinel control-plane ${options.action}`; const mutationAction = options.action === "apply" || options.action === "trigger-current"; if (options.confirm && mutationAction) { if (!options.wait) return renderAsyncSentinelJob(state, "control-plane", options.action, options.timeoutSeconds); @@ -512,7 +512,7 @@ function probeImageRegistry(state: SentinelCicdState, timeoutSeconds: number): R } function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = "hwlab nodes web-probe sentinel image build"; + const command = "web-probe sentinel image build"; const publish = runSentinelPublishJob(state, false, options.timeoutSeconds); const registry = probeImageRegistry(state, options.timeoutSeconds); const registryReady = record(registry.probe).present === true; @@ -531,8 +531,8 @@ function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extra warnings: sentinelElapsedWarnings(record(publish).elapsedMs), blocker: null, next: { - status: `bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, - controlPlaneTrigger: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, + status: `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, + controlPlaneTrigger: `bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, }, valuesRedacted: true, }; @@ -540,7 +540,7 @@ function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extra } function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = `hwlab nodes web-probe sentinel control-plane ${options.action}`; + const command = `web-probe sentinel control-plane ${options.action}`; const applyOnly = options.action === "apply"; const publish = applyOnly ? null : runSentinelPublishJob(state, true, options.timeoutSeconds); const flush = !applyOnly && record(publish).ok === true @@ -627,10 +627,10 @@ function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Ext function renderAsyncSentinelJob(state: SentinelCicdState, domain: "image" | "control-plane", action: string, timeoutSeconds: number): RenderedCliResult { const args = domain === "image" - ? ["hwlab", "nodes", "web-probe", "sentinel", "image", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)] - : ["hwlab", "nodes", "web-probe", "sentinel", "control-plane", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)]; + ? ["web-probe", "sentinel", "image", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)] + : ["web-probe", "sentinel", "control-plane", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)]; const job = startJob(`hwlab_nodes_${state.spec.lane}_web_probe_sentinel_${domain}_${action}`, ["bun", "scripts/cli.ts", ...args], `Run HWLAB ${state.spec.lane} web-probe sentinel ${domain} ${action} for node ${state.spec.nodeId}`); - const command = `hwlab nodes web-probe sentinel ${domain} ${action}`; + const command = `web-probe sentinel ${domain} ${action}`; const result = { ok: true, command, @@ -1121,17 +1121,17 @@ function controlPlaneNext(state: SentinelCicdState, action: WebProbeSentinelCont const node = state.spec.nodeId; const lane = state.spec.lane; return { - plan: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node ${node} --lane ${lane} --dry-run`, - status: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node ${node} --lane ${lane}`, - image: `bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node ${node} --lane ${lane}`, - triggerCurrent: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node ${node} --lane ${lane} --dry-run`, + plan: `bun scripts/cli.ts web-probe sentinel control-plane plan --node ${node} --lane ${lane} --dry-run`, + status: `bun scripts/cli.ts web-probe sentinel control-plane status --node ${node} --lane ${lane}`, + image: `bun scripts/cli.ts web-probe sentinel image status --node ${node} --lane ${lane}`, + triggerCurrent: `bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node ${node} --lane ${lane} --dry-run`, issue: "https://github.com/pikasTech/unidesk/issues/889", currentAction: action, }; } function runSentinelMaintenance(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = `hwlab nodes web-probe sentinel maintenance ${options.action}`; + const command = `web-probe sentinel maintenance ${options.action}`; const serviceHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds); if (options.action === "status") { const maintenance = callSentinelService(state, "GET", "/api/maintenance", null, options.timeoutSeconds); @@ -1206,7 +1206,7 @@ function runSentinelMaintenance(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = "hwlab nodes web-probe sentinel validate"; + const command = "web-probe sentinel validate"; const initialHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds); let quickVerify: Record | null = null; if (options.quickVerify) { @@ -1271,7 +1271,7 @@ function runSentinelValidate(state: SentinelCicdState, options: Extract): RenderedCliResult { - const command = `hwlab nodes web-probe sentinel report ${options.latest ? "--latest " : ""}--view ${options.view}`; + const command = `web-probe sentinel report ${options.latest ? "--latest " : ""}--view ${options.view}`; const query = new URLSearchParams({ view: options.view }); if (options.runId !== null) query.set("run", options.runId); if (options.traceId !== null) query.set("traceId", options.traceId); @@ -1284,12 +1284,12 @@ function runSentinelReport(state: SentinelCicdState, options: Extract[] = []; const startArgs = [ - "hwlab", "nodes", "web-probe", "observe", "start", + "web-probe", "observe", "start", "--node", state.spec.nodeId, "--lane", state.spec.lane, "--target-path", stringAt(scenario, "observeTargetPath"), @@ -1360,7 +1360,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou promptSource: prompts.summary, })); } - const args = ["hwlab", "nodes", "web-probe", "observe", "command", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--type", type, "--wait-ms", "55000", "--command-timeout-seconds", String(remainingSeconds(deadline, 55))]; + const args = ["web-probe", "observe", "command", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--type", type, "--wait-ms", "55000", "--command-timeout-seconds", String(remainingSeconds(deadline, 55))]; if (type === "selectProvider") args.push("--provider", stringAt(item, "provider")); if (type === "sendPrompt") { args.push("--text", prompts.prompts[promptIndex % prompts.prompts.length] ?? ""); @@ -1399,7 +1399,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou } } } - const analysis = runChildCli(["hwlab", "nodes", "web-probe", "observe", "analyze", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", String(remainingSeconds(deadline, 120))], remainingSeconds(deadline, 120)); + const analysis = runChildCli(["web-probe", "observe", "analyze", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", String(remainingSeconds(deadline, 120))], remainingSeconds(deadline, 120)); steps.push({ phase: "observe-analyze", ok: analysis.ok, result: analysis.result }); const indexEntry = readLocalObserveIndex(observerId); const artifactSummary = indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, remainingSeconds(deadline, 30)); @@ -1446,7 +1446,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { const cleanupSteps: Record[] = []; if (input.promptIndex > 0) { const cancel = runChildCli([ - "hwlab", "nodes", "web-probe", "observe", "command", input.observerId, + "web-probe", "observe", "command", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--type", "cancel", @@ -1456,7 +1456,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { cleanupSteps.push({ phase: "observe-cancel-after-failure", ok: cancel.ok, result: cancel.result }); } const stop = runChildCli([ - "hwlab", "nodes", "web-probe", "observe", "stop", input.observerId, + "web-probe", "observe", "stop", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--force", @@ -1464,7 +1464,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { ], 30); cleanupSteps.push({ phase: "observe-stop-after-failure", ok: stop.ok, result: stop.result }); const analysis = runChildCli([ - "hwlab", "nodes", "web-probe", "observe", "analyze", input.observerId, + "web-probe", "observe", "analyze", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", "55", @@ -1815,7 +1815,7 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st } function collectObserveView(state: SentinelCicdState, observerId: string, view: "turn-summary" | "trace-frame", turn: number | null, timeoutSeconds: number): Record { - const args = ["hwlab", "nodes", "web-probe", "observe", "collect", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--view", view, "--command-timeout-seconds", String(Math.max(5, Math.min(timeoutSeconds, 55))), "--raw", "--compact-raw"]; + const args = ["web-probe", "observe", "collect", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--view", view, "--command-timeout-seconds", String(Math.max(5, Math.min(timeoutSeconds, 55))), "--raw", "--compact-raw"]; if (turn !== null) args.push("--turn", String(turn)); const result = runChildCli(args, timeoutSeconds); const payload = cliDataPayload(result.parsed); @@ -2124,7 +2124,7 @@ function serviceUnavailableBlocker(state: SentinelCicdState): Record { const node = state.spec.nodeId; const lane = state.spec.lane; return { - validate: `bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node ${node} --lane ${lane}`, - quickVerify: `bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node ${node} --lane ${lane} --quick-verify --confirm --wait`, - maintenanceStart: `bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance start --node ${node} --lane ${lane} --confirm --wait`, - maintenanceStop: `bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node ${node} --lane ${lane} --confirm --wait`, - report: `bun scripts/cli.ts hwlab nodes web-probe sentinel report --node ${node} --lane ${lane} --view summary`, + validate: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane}`, + quickVerify: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane} --quick-verify --confirm --wait`, + maintenanceStart: `bun scripts/cli.ts web-probe sentinel maintenance start --node ${node} --lane ${lane} --confirm --wait`, + maintenanceStop: `bun scripts/cli.ts web-probe sentinel maintenance stop --node ${node} --lane ${lane} --confirm --wait`, + report: `bun scripts/cli.ts web-probe sentinel report --node ${node} --lane ${lane} --view summary`, }; } diff --git a/scripts/src/hwlab-node-web-sentinel-config.ts b/scripts/src/hwlab-node-web-sentinel-config.ts index a9255f14..a9129af2 100644 --- a/scripts/src/hwlab-node-web-sentinel-config.ts +++ b/scripts/src/hwlab-node-web-sentinel-config.ts @@ -1,5 +1,5 @@ // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. -// Responsibility: Redacted YAML configRef graph for hwlab nodes web-probe sentinel plan/status. +// Responsibility: Redacted YAML configRef graph for web-probe sentinel plan/status. import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { rootPath } from "./config"; @@ -167,7 +167,7 @@ const REQUIRED_TARGET_SHAPES: Record { return { - plan: `bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node ${node} --lane ${lane} --dry-run`, - status: `bun scripts/cli.ts hwlab nodes web-probe sentinel status --node ${node} --lane ${lane}`, + plan: `bun scripts/cli.ts web-probe sentinel plan --node ${node} --lane ${lane} --dry-run`, + status: `bun scripts/cli.ts web-probe sentinel status --node ${node} --lane ${lane}`, }; } diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index 351038bd..0f195293 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -335,7 +335,7 @@ function serviceHealth(config: WebProbeSentinelServiceConfig, db: Database, sche checks.analyzer = { ok: true, source: "existing observe analyze CLI command", - command: `bun scripts/cli.ts hwlab nodes web-probe observe analyze --node ${config.node} --lane ${config.lane} --state-dir `, + command: `bun scripts/cli.ts web-probe observe analyze --node ${config.node} --lane ${config.lane} --state-dir `, }; const ok = Object.values(checks).every((check) => check.ok === true); return { ok, status: ok ? "healthy" : "degraded", node: config.node, lane: config.lane, checks, valuesRedacted: true }; @@ -367,7 +367,7 @@ function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario const start: CommandPlanStep = { phase: "observe-start", argv: [ - "bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "start", + "bun", "scripts/cli.ts", "web-probe", "observe", "start", "--node", config.node, "--lane", config.lane, "--target-path", targetPath, @@ -379,14 +379,14 @@ function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario }; const commands = arrayAt(scenario, "commandSequence").map((item) => { const type = stringAt(item, "type"); - const argv = ["bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "command", "", "--type", type]; + const argv = ["bun", "scripts/cli.ts", "web-probe", "observe", "command", "", "--type", type]; if (type === "selectProvider") argv.push("--provider", stringAt(item, "provider")); if (type === "sendPrompt") argv.push("--text-stdin"); return { phase: `observe-command-${type}`, argv, stdinSource: type === "sendPrompt" ? "prompt-source" : "none" } satisfies CommandPlanStep; }); const analyze: CommandPlanStep = { phase: "observe-analyze", - argv: ["bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "analyze", ""], + argv: ["bun", "scripts/cli.ts", "web-probe", "observe", "analyze", ""], stdinSource: "none", }; return [start, ...commands, analyze]; diff --git a/scripts/src/hwlab-node/entry.ts b/scripts/src/hwlab-node/entry.ts index 504b9426..f64ebf3e 100644 --- a/scripts/src/hwlab-node/entry.ts +++ b/scripts/src/hwlab-node/entry.ts @@ -39,8 +39,9 @@ import { nodeRuntimeUnsupportedAction } from "./runtime-common"; import { runNodeEndpointBridge } from "./secret-scripts"; import { nodeRuntimeGitMirrorRun, nodeRuntimeGitMirrorStatus, nodeScopedFullOutput, withNodeRuntimeGitMirrorRendered } from "./status"; import { assertNodeId, positiveIntegerOption, requiredOption, stripOption } from "./utils"; -import { parseNodeWebProbeOptions, rewriteDelegatedNodeResult, startNodeDelegatedJob } from "./web-probe"; -import { assertKnownOptions, runNodeWebProbe } from "./web-probe-observe"; +import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe"; +import { rewriteDelegatedNodeResult, startNodeDelegatedJob } from "./web-probe"; +import { assertKnownOptions } from "./web-probe-observe"; export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help"; @@ -419,8 +420,7 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom return runHwlabTestAccountsCommand(args.slice(1)); } if (domain === "web-probe") { - if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeWebProbeHelp(); - return runNodeWebProbe(parseNodeWebProbeOptions(args.slice(1))); + return legacyHwlabNodeWebProbeUnsupported(args.slice(1)); } if (domain === "observability") { if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeObservabilityHelp(); @@ -431,7 +431,7 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom return runNodeDelegatedDomain(_config, domain, args.slice(1)); } if (domain !== "secret") { - return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts, hwlab nodes web-probe" }; + return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" }; } const options = parseSecretOptions(args.slice(1)); return runNodeSecret(options); diff --git a/scripts/src/hwlab-node/observability.ts b/scripts/src/hwlab-node/observability.ts index f358272f..67415427 100644 --- a/scripts/src/hwlab-node/observability.ts +++ b/scripts/src/hwlab-node/observability.ts @@ -248,7 +248,7 @@ export function nodeObservabilityPerformanceSummary(options: NodeObservabilityOp degradedReason: material.error ?? "web-login-secret-unavailable", next: { secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name bootstrap-admin`, - webProbe: `bun scripts/cli.ts hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`, + webProbe: `bun scripts/cli.ts web-probe run --node ${options.node} --lane ${options.lane}`, }, }; } @@ -287,7 +287,7 @@ export function nodeObservabilityPerformanceSummary(options: NodeObservabilityOp ? report.error : result.exitCode === 0 ? "web-performance-summary-not-ready" : "web-performance-summary-probe-failed", next: { - webProbe: `bun scripts/cli.ts hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`, + webProbe: `bun scripts/cli.ts web-probe run --node ${options.node} --lane ${options.lane}`, workbenchSummary: `bun scripts/cli.ts hwlab nodes observability workbench-summary --node ${options.node} --lane ${options.lane}`, apiMetrics: `bun scripts/cli.ts hwlab nodes observability status --node ${options.node} --lane ${options.lane} --full`, }, diff --git a/scripts/src/hwlab-node/render.ts b/scripts/src/hwlab-node/render.ts index d483145b..6076712e 100644 --- a/scripts/src/hwlab-node/render.ts +++ b/scripts/src/hwlab-node/render.ts @@ -385,7 +385,7 @@ export function nodeRuntimePublicProbeDiagnostic(publicReady: boolean, targetHos affectsUserEntry: true, targetHostReady: targetReady, message: "control-plane public probe failed; treat this as a public endpoint readiness failure before using web-probe closeout evidence.", - nextAction: "run hwlab nodes web-probe run for the same node/lane after checking publicProbe.web and publicProbe.apiHealth", + nextAction: "run web-probe run for the same node/lane after checking publicProbe.web and publicProbe.apiHealth", }; } if (targetHost.probeAvailable !== true) { @@ -542,7 +542,7 @@ export function summarizeNodeRuntimeControlPlaneStatus(status: Record, sco return `bun scripts/cli.ts hwlab nodes control-plane cleanup-runs --node ${scoped.node} --lane ${scoped.lane} --min-age-minutes 60 --limit 20 --dry-run`; } if (reason === "public-probe-not-ready") { - return `bun scripts/cli.ts hwlab nodes web-probe run --node ${scoped.node} --lane ${scoped.lane}`; + return `bun scripts/cli.ts web-probe run --node ${scoped.node} --lane ${scoped.lane}`; } if (reason === "git-mirror-pending-flush") { return `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`; diff --git a/scripts/src/hwlab-node/web-observe-render.ts b/scripts/src/hwlab-node/web-observe-render.ts index b0e4666e..db05b879 100644 --- a/scripts/src/hwlab-node/web-observe-render.ts +++ b/scripts/src/hwlab-node/web-observe-render.ts @@ -84,7 +84,7 @@ export function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserve export function withWebObserveAnalyzeRendered(value: Record): RenderedCliResult { return { ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe analyze", + command: typeof value.command === "string" ? value.command : "web-probe observe analyze", contentType: "text/plain", renderedText: renderWebObserveAnalyzeTable(value), }; @@ -321,7 +321,7 @@ export function renderWebObserveAnalyzeTable(value: Record): st webObserveShort(webObserveArray(item.slowSamples).map((sample) => webObserveText(record(sample)?.otelTraceId)).filter((text) => text !== "-").join(",") || "-", 36), ]); const lines = [ - `hwlab nodes web-probe observe analyze (${webObserveText(value.status)})`, + `web-probe observe analyze (${webObserveText(value.status)})`, "", ...(value.ok === false ? [ "Blocked detail:", @@ -916,7 +916,7 @@ export function upsertWebObserveIndexEntry(entry: WebObserveIndexEntry): Record< ok: true, id: entry.id, path, - statusCommand: `bun scripts/cli.ts hwlab nodes web-probe observe status ${entry.id}`, + statusCommand: `bun scripts/cli.ts web-probe observe status ${entry.id}`, valuesRedacted: true, }; } catch (error) { @@ -925,7 +925,7 @@ export function upsertWebObserveIndexEntry(entry: WebObserveIndexEntry): Record< id: entry.id, path, error: error instanceof Error ? error.message : String(error), - fallback: `bun scripts/cli.ts hwlab nodes web-probe observe status --node ${entry.node} --lane ${entry.lane} --state-dir ${entry.stateDir}`, + fallback: `bun scripts/cli.ts web-probe observe status --node ${entry.node} --lane ${entry.lane} --state-dir ${entry.stateDir}`, valuesRedacted: true, }; } @@ -964,16 +964,16 @@ export function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOpti export function webObserveCommandLabel(action: NodeWebProbeObserveAction, options: Pick): string { const id = webObserveIdFromOptions(options); return id === null - ? `hwlab nodes web-probe observe ${action} --node ${options.node} --lane ${options.lane}` - : `hwlab nodes web-probe observe ${action} ${id}`; + ? `web-probe observe ${action} --node ${options.node} --lane ${options.lane}` + : `web-probe observe ${action} ${id}`; } export function webObserveNextCommands(id: string): Record { return { - status: `bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - command: `bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, - stop: `bun scripts/cli.ts hwlab nodes web-probe observe stop ${id}`, - analyze: `bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, + status: `bun scripts/cli.ts web-probe observe status ${id}`, + command: `bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`, + stop: `bun scripts/cli.ts web-probe observe stop ${id}`, + analyze: `bun scripts/cli.ts web-probe observe analyze ${id}`, }; } @@ -1058,7 +1058,7 @@ export function renderWebProbeRunResult(result: Record): Record "", "NEXT", ` report: ${reportPath}`, - ` rerun: ${result.command ?? `hwlab nodes web-probe run --node ${result.node ?? "-"} --lane ${result.lane ?? "-"}`}`, + ` rerun: ${result.command ?? `web-probe run --node ${result.node ?? "-"} --lane ${result.lane ?? "-"}`}`, ].join("\n"); return withWebObserveRendered(result, renderedText); } @@ -1116,10 +1116,10 @@ export function renderWebObserveStartResult(result: Record): Re ...blockedRows, "", "NEXT", - ` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - ` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, - ` command: bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, - ` stop: bun scripts/cli.ts hwlab nodes web-probe observe stop ${id}`, + ` status: bun scripts/cli.ts web-probe observe status ${id}`, + ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`, + ` command: bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`, + ` stop: bun scripts/cli.ts web-probe observe stop ${id}`, ].join("\n"); return withWebObserveRendered(result, renderedText); } @@ -1174,9 +1174,9 @@ export function renderWebObserveStatusResult(result: Record): R ...resultSection, "", "NEXT", - ` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - ` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, - ` command: bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, + ` status: bun scripts/cli.ts web-probe observe status ${id}`, + ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`, + ` command: bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`, ].join("\n"); return withWebObserveRendered(result, renderedText); } @@ -1203,8 +1203,8 @@ export function renderWebObserveCommandResult(result: Record): ), "", "NEXT", - ` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - ` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, + ` status: bun scripts/cli.ts web-probe observe status ${id}`, + ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`, ].join("\n"); return withWebObserveRendered(result, renderedText); } @@ -1396,7 +1396,7 @@ export function renderWebObserveAnalyzeResult(result: Record): ` md: ${analysis.reportMdPath ?? "-"}`, "", "NEXT", - ` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, + ` status: bun scripts/cli.ts web-probe observe status ${id}`, ].join("\n"); return withWebObserveRendered(result, renderedText); } @@ -1464,7 +1464,7 @@ export function renderWebProbeScriptResult(result: Record): Rec "", "NEXT", ` report: ${reportLoad.path ?? probe.reportPath ?? "-"}`, - ` rerun: ${result.command ?? `hwlab nodes web-probe script --node ${result.node ?? "-"} --lane ${result.lane ?? "-"}`}`, + ` rerun: ${result.command ?? `web-probe script --node ${result.node ?? "-"} --lane ${result.lane ?? "-"}`}`, ].join("\n"); return withWebObserveRendered(result, renderedText); } diff --git a/scripts/src/hwlab-node/web-observe-scripts.ts b/scripts/src/hwlab-node/web-observe-scripts.ts index 0f9f27c2..60b26d8a 100644 --- a/scripts/src/hwlab-node/web-observe-scripts.ts +++ b/scripts/src/hwlab-node/web-observe-scripts.ts @@ -79,7 +79,7 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string, const commandsFailed=commandSummary('failed'); const diagnostics={heartbeatAgeSeconds,heartbeatStale,heartbeatStaleAfterSeconds:Math.round(staleAfterMs/1000),heartbeatUpdatedAt:updatedRaw,terminal,processAlive:alive,effectiveLiveness:heartbeatStale?'stale':alive?'alive':'not-running',commandBacklog:commandsPending.count+commandsProcessing.count,oldestPendingAgeSeconds:commandsPending.oldestAgeSeconds,oldestProcessingAgeSeconds:commandsProcessing.oldestAgeSeconds,valuesRedacted:true}; const commands={pendingCount:commandsPending.count,processingCount:commandsProcessing.count,abandonedCount:commandsAbandoned.count,failedCount:commandsFailed.count,pending:commandsPending.items,processing:commandsProcessing.items,abandoned:commandsAbandoned.items.slice(0,6),failed:commandsFailed.items.slice(0,6),valuesRedacted:true}; - console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:dir,pid:pidText?Number(pidText):null,processAlive:alive,manifest:compactManifest(manifest),heartbeat:compactHeartbeat(heartbeat,diagnostics),diagnostics,commands,tails:{control:tailJsonl('control.jsonl').map(compactControl),samples:tailJsonl('samples.jsonl').map(compactSample),network:tailJsonl('network.jsonl').map(compactNetwork)},next:{command:'bun scripts/cli.ts hwlab nodes web-probe observe command --node '+node+' --lane '+lane+' --state-dir '+dir+' --type mark --label checkpoint',stop:'bun scripts/cli.ts hwlab nodes web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir,forceStop:'bun scripts/cli.ts hwlab nodes web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir+' --force',analyze:'bun scripts/cli.ts hwlab nodes web-probe observe analyze --node '+node+' --lane '+lane+' --state-dir '+dir},valuesRedacted:true})); + console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:dir,pid:pidText?Number(pidText):null,processAlive:alive,manifest:compactManifest(manifest),heartbeat:compactHeartbeat(heartbeat,diagnostics),diagnostics,commands,tails:{control:tailJsonl('control.jsonl').map(compactControl),samples:tailJsonl('samples.jsonl').map(compactSample),network:tailJsonl('network.jsonl').map(compactNetwork)},next:{command:'bun scripts/cli.ts web-probe observe command --node '+node+' --lane '+lane+' --state-dir '+dir+' --type mark --label checkpoint',stop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir,forceStop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir+' --force',analyze:'bun scripts/cli.ts web-probe observe analyze --node '+node+' --lane '+lane+' --state-dir '+dir},valuesRedacted:true})); `)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`; } @@ -539,7 +539,7 @@ export function runNodeWebProbeScript( return renderWebProbeScriptResult({ ok: passed, status: passed ? "pass" : "blocked", - command: `hwlab nodes web-probe script --node ${options.node} --lane ${options.lane}`, + command: `web-probe script --node ${options.node} --lane ${options.lane}`, node: options.node, lane: options.lane, workspace: spec.workspace, diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index da25fd9b..d311def0 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -434,8 +434,8 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record\"$state_dir/pid\"", "sleep 1", - `node -e ${shellQuote("const fs=require('fs'); const dir=process.argv[1]; const read=(n)=>{try{return JSON.parse(fs.readFileSync(dir+'/'+n,'utf8'))}catch{return null}}; const pid=fs.existsSync(dir+'/pid')?fs.readFileSync(dir+'/pid','utf8').trim():null; console.log(JSON.stringify({ok:true,command:'web-probe-observe start',jobId:process.argv[2],stateDir:dir,pid:Number(pid)||null,manifestPath:dir+'/manifest.json',heartbeat:read('heartbeat.json'),manifest:read('manifest.json'),statusCommand:'bun scripts/cli.ts hwlab nodes web-probe observe status --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,stopCommand:'bun scripts/cli.ts hwlab nodes web-probe observe stop --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,valuesRedacted:true},null,2))")} "$state_dir" ${shellQuote(jobId)} ${shellQuote(options.node)} ${shellQuote(options.lane)}`, + `node -e ${shellQuote("const fs=require('fs'); const dir=process.argv[1]; const read=(n)=>{try{return JSON.parse(fs.readFileSync(dir+'/'+n,'utf8'))}catch{return null}}; const pid=fs.existsSync(dir+'/pid')?fs.readFileSync(dir+'/pid','utf8').trim():null; console.log(JSON.stringify({ok:true,command:'web-probe-observe start',jobId:process.argv[2],stateDir:dir,pid:Number(pid)||null,manifestPath:dir+'/manifest.json',heartbeat:read('heartbeat.json'),manifest:read('manifest.json'),statusCommand:'bun scripts/cli.ts web-probe observe status --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,stopCommand:'bun scripts/cli.ts web-probe observe stop --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,valuesRedacted:true},null,2))")} "$state_dir" ${shellQuote(jobId)} ${shellQuote(options.node)} ${shellQuote(options.lane)}`, ].join("\n"); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const started = parseJsonObject(result.stdout); @@ -948,7 +948,7 @@ export function runNodeWebProbeObserveStart( return renderWebObserveStartResult({ ok: result.exitCode === 0 && started?.ok === true, status: result.exitCode === 0 && started?.ok === true ? "started" : "blocked", - command: `hwlab nodes web-probe observe start --node ${options.node} --lane ${options.lane}`, + command: `web-probe observe start --node ${options.node} --lane ${options.lane}`, node: options.node, lane: options.lane, workspace: spec.workspace, @@ -1653,8 +1653,8 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption : "observe analyze failed before producing a compact report; inspect analyzer stdout/stderr artifacts under the observer analysis directory", }], next: { - collectAnalyzerStdout: options.stateDir ? `bun scripts/cli.ts hwlab nodes web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stdout.json` : null, - collectAnalyzerStderr: options.stateDir ? `bun scripts/cli.ts hwlab nodes web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stderr.log` : null, + collectAnalyzerStdout: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stdout.json` : null, + collectAnalyzerStderr: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stderr.log` : null, valuesRedacted: true, }, valuesRedacted: true, diff --git a/scripts/src/web-probe.ts b/scripts/src/web-probe.ts new file mode 100644 index 00000000..fbdf7f5e --- /dev/null +++ b/scripts/src/web-probe.ts @@ -0,0 +1,32 @@ +// SPEC: PJ2026-01010305 71FREQ预装 draft-2026-06-26-71freq-v03-hwpod-preinstall. +// Responsibility: Top-level web-probe CLI adapter and legacy hwlab nodes web-probe migration guard. +import type { Config } from "./config"; +import type { RenderedCliResult } from "./output"; +import { hwlabNodeWebProbeHelp } from "./hwlab-node-help"; +import { parseNodeWebProbeOptions } from "./hwlab-node/web-probe"; +import { runNodeWebProbe } from "./hwlab-node/web-probe-observe"; + +export function webProbeHelp(): Record { + return hwlabNodeWebProbeHelp(); +} + +export async function runWebProbeCommand(_config: Config, args: string[]): Promise | RenderedCliResult> { + if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return webProbeHelp(); + return runNodeWebProbe(parseNodeWebProbeOptions(args)); +} + +export function legacyHwlabNodeWebProbeUnsupported(args: string[]): Record { + const filtered = args.filter((item) => item !== "help" && item !== "--help" && item !== "-h"); + const suffix = filtered.length === 0 ? "" : ` ${filtered.join(" ")}`; + return { + ok: false, + status: "unsupported", + command: `hwlab nodes web-probe${args.length === 0 ? "" : ` ${args.join(" ")}`}`.trim(), + message: "`hwlab nodes web-probe` has moved to top-level `web-probe`; the old path is no longer an executable alias.", + migration: { + canonicalCommand: `bun scripts/cli.ts web-probe${suffix}`, + help: "bun scripts/cli.ts web-probe --help", + }, + valuesPrinted: false, + }; +}