feat: move web-probe to top-level cli (#933)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 09:57:14 +08:00
committed by GitHub
parent 0328967e58
commit 241a2d511f
18 changed files with 249 additions and 274 deletions
+42 -42
View File
@@ -21,15 +21,15 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H
- Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutationWeb 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工作台` 的“破坏性投影权”。 - Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutationWeb 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 作为通过条件。 - fake-server 不是第二后端:它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*``/v1/*``/health*` 请求应失败并暴露 path;不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。
- 真实数据优先:fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界,并标明 `derivedFrom``syntheticReason` - 真实数据优先: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。 - Master server 禁重型验证:不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。
- 禁止裸写 PlaywrightUniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `hwlab nodes web-probe run|script|observe`;不得直接写 `trans <route> playwright` heredoc、`playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有非 HWLAB 外站截图/PDF 等 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。 - 禁止裸写 PlaywrightUniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`;不得直接写 `trans <route> 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 指向该目标时使用。 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。 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 从真实采集样本脱敏产生。 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 中新增竞争事实。 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 作为证据回传,不进入源码仓库。 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 验收优先使用受控入口: 线上 Cloud Web DOM 验收优先使用受控入口:
```bash ```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 入口分三类: web-probe 入口分三类:
@@ -54,7 +54,7 @@ web-probe 入口分三类:
需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright 需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright
```bash ```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 }) => { export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => {
await gotoStable('/workbench'); await gotoStable('/workbench');
const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5'); const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5');
@@ -82,27 +82,27 @@ JS
长程 Workbench 观测使用 `observe` 命令组: 长程 Workbench 观测使用 `observe` 命令组:
```bash ```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 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 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 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 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 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 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 command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'
long prompt long prompt
EOF EOF
bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx --tail-lines 6 bun scripts/cli.ts 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 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 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 web-probe observe stop webobs-xxxx
bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx bun scripts/cli.ts 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 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 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 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 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 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 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 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 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 项目管理 / MDTODO 页面同样优先使用 `observe`,不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`,同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch
```bash ```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 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 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 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 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 web-probe observe command webobs-xxxx --type reindexMdtodoSource
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id> bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id>
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref> bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref>
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx> bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx>
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance' bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command' bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command'
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask' bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask'
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task' bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task'
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref> bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref>
bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref> bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref>
bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view project-mdtodo-summary bun scripts/cli.ts 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 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 手段。 `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 ## 受控 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 或多来源仲裁不能作为修复。 `web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告:可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称;点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。
@@ -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) | | 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 为准。 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 为准。
+14
View File
@@ -316,6 +316,20 @@ async function main(): Promise<void> {
return; 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 (top === "hwlab") {
if (sub === "node" || sub === "nodes") { if (sub === "node" || sub === "nodes") {
const { runHwlabNodeCommand } = await import("./src/hwlab-node"); const { runHwlabNodeCommand } = await import("./src/hwlab-node");
+20 -7
View File
@@ -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: "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: "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: "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 <node> --lane <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 <node> --lane <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 <node> --lane <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: "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: "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." }, { 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 { function hwlabNodeHelpSummary(): unknown {
return { return {
command: "hwlab nodes control-plane|git-mirror|observability|secret|test-accounts|web-probe --node <node> --lane <lane>", command: "hwlab nodes control-plane|git-mirror|observability|secret|test-accounts --node <node> --lane <lane>",
output: "json", output: "json",
usage: [ usage: [
"bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03", "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 <secret>", "bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name <secret>",
"bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03", "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 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 <node> --lane <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<unknown | nul
if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary()); if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary());
if (top === "platform-db") return platformDbHelp(); if (top === "platform-db") return platformDbHelp();
if (top === "secrets") return secretsHelp(); 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") { 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()); return loadHelp(async () => (await import("./hwlab-node-control-plane")).hwlabNodeControlPlaneInfraHelp(), hwlabNodeHelpSummary());
} }
if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "test-accounts") { if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "test-accounts") {
return loadHelp(async () => (await import("./hwlab-test-accounts")).hwlabTestAccountsHelp(), hwlabNodeHelpSummary()); 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 === "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" && sub === "g14") return loadHelp(async () => (await import("./hwlab-g14")).hwlabG14Help(), hwlabG14HelpSummary());
if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary()); if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary());
+38 -122
View File
@@ -1,80 +1,33 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. // 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"; import { hwlabRuntimeLaneConfigPath } from "./hwlab-node-lanes";
export function hwlabNodeHelp(): Record<string, unknown> { export function hwlabNodeHelp(): Record<string, unknown> {
return { return {
ok: true, ok: true,
command: "hwlab nodes", 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(), configPath: hwlabRuntimeLaneConfigPath(),
examples: [ examples: [
"bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03", "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",
"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 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 status --node G14 --lane v03 --name <secret>",
"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 test-accounts status --node D601 --lane v03", "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 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: [ notes: [
"`trigger-current` skips an already succeeded PipelineRun for the same HWLAB source commit by default.", "`web-probe` is no longer under `hwlab nodes`; use `bun scripts/cli.ts web-probe ...`.",
"`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.", "`--node` and `--lane` remain data arguments resolved from YAML for node/lane operations.",
"`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.", "`trigger-current --confirm --wait` is the one-command CICD path for current node/lane runtime publish.",
"`--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."
], ],
}; };
} }
@@ -82,73 +35,36 @@ export function hwlabNodeHelp(): Record<string, unknown> {
export function hwlabNodeWebProbeHelp(): Record<string, unknown> { export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
return { return {
ok: true, ok: true,
command: "hwlab nodes web-probe", command: "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.", description: "Run target node/lane HWLAB Cloud Web probes with YAML-selected origin and one-shot bootstrap Web credentials.",
examples: [ examples: [
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000", "bun scripts/cli.ts 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 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 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 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 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 web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api", "bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task R1.1",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'", "bun scripts/cli.ts web-probe observe status webobs-xxxx",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'\nlong prompt\nEOF", "bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'", "bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel", "bun scripts/cli.ts web-probe observe analyze webobs-xxxx",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type gotoProjectMdtodo", "bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run",
"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 web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait --release-id <id>",
"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 <id>",
"bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait --release-id <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",
], ],
actions: { actions: {
run: "Run the repo-owned scripts/web-live-dom-probe.mjs helper.", 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.", 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 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.", observe: "Start, inspect, control, stop, collect, and analyze a long-running observer that writes JSONL artifacts.",
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.", sentinel: "Render and operate the YAML-first web-probe sentinel wrapper, image, GitOps, maintenance and report views.",
}, },
notes: [ 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.", "Default URL, browser proxy mode, observe/analyze thresholds, and project-management command allowlist come from config/hwlab-node-lanes.yaml webProbe.",
"Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.", "observe is passive by default; user actions must be explicit observe command entries in control.jsonl.",
"Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.", "After observe start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.",
"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.", "collect views render bounded summaries from existing artifacts and do not create a second source of truth.",
"observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.", "analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json.",
"observe status reports runner liveness separately from process existence, including heartbeat age, stale threshold, command backlog, and abandoned command counts.", "Issue evidence should cite observer id, stateDir, report SHA, screenshot SHA, command ids and concise summaries, not prompt/provider/secret payloads.",
"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.",
], ],
}; };
} }
@@ -299,7 +299,7 @@ function renderTurnSummary(rows){
const title='Workbench Session '+anchorSessionId+' observer '+(manifest.jobId||'-')+driftSuffix; const title='Workbench Session '+anchorSessionId+' observer '+(manifest.jobId||'-')+driftSuffix;
const header='轮次 用户消息 Trace 状态 耗时/最近 标记 Final Response'; 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'); 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||'<observer>')+' --view trace-frame --trace-id <traceId> --sample-seq <seq>'].join('\\n'); return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts web-probe observe collect '+(manifest.jobId||'<observer>')+' --view trace-frame --trace-id <traceId> --sample-seq <seq>'].join('\\n');
} }
function diagnosticRows(label,items,limit=6){ function diagnosticRows(label,items,limit=6){
const selected=items.slice(-limit); const selected=items.slice(-limit);
+11 -11
View File
@@ -1,13 +1,13 @@
// SPEC: PJ2026-01040111 long-running Workbench observation. // SPEC: PJ2026-01040111 long-running Workbench observation.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. // 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 type { RenderedCliResult } from "./output";
import { renderWebObserveWrapperContract } from "./hwlab-node-web-observe-wrapper-render"; import { renderWebObserveWrapperContract } from "./hwlab-node-web-observe-wrapper-render";
export function withWebObserveStatusRendered(value: Record<string, unknown>): RenderedCliResult { export function withWebObserveStatusRendered(value: Record<string, unknown>): RenderedCliResult {
return { return {
ok: value.ok !== false, 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", contentType: "text/plain",
renderedText: renderWebObserveStatusTable(value), renderedText: renderWebObserveStatusTable(value),
}; };
@@ -16,7 +16,7 @@ export function withWebObserveStatusRendered(value: Record<string, unknown>): Re
export function withWebObserveCommandRendered(value: Record<string, unknown>): RenderedCliResult { export function withWebObserveCommandRendered(value: Record<string, unknown>): RenderedCliResult {
return { return {
ok: value.ok !== false, 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", contentType: "text/plain",
renderedText: renderWebObserveCommandTable(value), renderedText: renderWebObserveCommandTable(value),
}; };
@@ -25,7 +25,7 @@ export function withWebObserveCommandRendered(value: Record<string, unknown>): R
export function withWebObserveCollectRendered(value: Record<string, unknown>): RenderedCliResult { export function withWebObserveCollectRendered(value: Record<string, unknown>): RenderedCliResult {
return { return {
ok: value.ok !== false, 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", contentType: "text/plain",
renderedText: renderWebObserveCollectTable(value), renderedText: renderWebObserveCollectTable(value),
}; };
@@ -63,7 +63,7 @@ function renderWebObserveStatusTable(value: Record<string, unknown>): string {
const heartbeatStale = diagnostics?.heartbeatStale ?? heartbeat?.stale; const heartbeatStale = diagnostics?.heartbeatStale ?? heartbeat?.stale;
const heartbeatLiveness = diagnostics?.effectiveLiveness ?? heartbeat?.effectiveLiveness; const heartbeatLiveness = diagnostics?.effectiveLiveness ?? heartbeat?.effectiveLiveness;
const lines = [ const lines = [
`hwlab nodes web-probe observe status (${webObserveText(value.status)})`, `web-probe observe status (${webObserveText(value.status)})`,
"", "",
...renderWebObserveWrapperContract(value), ...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "LIVENESS", "ALIVE", "PID", "SAMPLE", "CMD_SEQ", "HB_AGE_S", "STALE", "UPDATED", "TARGET"], [[ 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, unknown>): string {
const id = webObserveText(value.id); const id = webObserveText(value.id);
const full = value.full === true; const full = value.full === true;
const lines = [ const lines = [
`hwlab nodes web-probe observe command (${webObserveText(value.status)})`, `web-probe observe command (${webObserveText(value.status)})`,
"", "",
...renderWebObserveWrapperContract(value), ...renderWebObserveWrapperContract(value),
webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[ webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[
@@ -211,8 +211,8 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
] : []), ] : []),
"", "",
"Next:", "Next:",
` bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, ` bun scripts/cli.ts web-probe observe status ${id}`,
` bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, ` bun scripts/cli.ts web-probe observe analyze ${id}`,
"", "",
"Disclosure:", "Disclosure:",
" default view is a bounded command result; pass --full to expose the complete command result JSON.", " 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, unknown>): string {
const collect = nullableRecord(value.collect); const collect = nullableRecord(value.collect);
if (typeof collect?.renderedText === "string") { if (typeof collect?.renderedText === "string") {
return [ return [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, `web-probe observe collect (${webObserveText(value.status)})`,
"", "",
...renderWebObserveWrapperContract(value), ...renderWebObserveWrapperContract(value),
collect.renderedText, collect.renderedText,
@@ -261,7 +261,7 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
const jsonFindingDetail = record(jsonSummary.findingDetail); const jsonFindingDetail = record(jsonSummary.findingDetail);
const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : []; const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : [];
const lines = [ const lines = [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, `web-probe observe collect (${webObserveText(value.status)})`,
"", "",
...renderWebObserveWrapperContract(value), ...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[ webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[
@@ -467,7 +467,7 @@ function renderWebObserveProjectCollectTable(value: Record<string, unknown>, col
const launches = Array.isArray(collect.launches) ? collect.launches.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-4) : []; const launches = Array.isArray(collect.launches) ? collect.launches.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-4) : [];
const findings = Array.isArray(collect.findings) ? collect.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 6) : []; const findings = Array.isArray(collect.findings) ? collect.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 6) : [];
const lines = [ const lines = [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, `web-probe observe collect (${webObserveText(value.status)})`,
"", "",
...renderWebObserveWrapperContract(value), ...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[ webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[
@@ -1,5 +1,5 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. // 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"; export type WebObserveWrapperAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";
@@ -92,7 +92,7 @@ export function buildWebObserveWrapperContract(input: WebObserveWrapperInput): R
id: observerId, id: observerId,
jobId: input.jobId ?? observerId, jobId: input.jobId ?? observerId,
cli: { cli: {
family: "bun scripts/cli.ts hwlab nodes web-probe observe", family: "bun scripts/cli.ts web-probe observe",
commandShape: webObserveWrapperCommandShape(input), commandShape: webObserveWrapperCommandShape(input),
verb: input.action, verb: input.action,
valuesRedacted: true, valuesRedacted: true,
@@ -185,7 +185,7 @@ function artifactPath(stateDir: string | null, relative: string): string | null
} }
function webObserveWrapperCommandShape(input: WebObserveWrapperInput): string { 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 observerId = input.id ?? input.jobId;
const parts = observerId === null const parts = observerId === null
? [...base, "--node", input.node, "--lane", input.lane] ? [...base, "--node", input.node, "--lane", input.lane]
+35 -35
View File
@@ -172,7 +172,7 @@ export function runWebProbeSentinelCommand(spec: HwlabRuntimeLaneSpec, options:
} }
function runSentinelImage(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "image" }>): RenderedCliResult { function runSentinelImage(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "image" }>): 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.action === "build" && options.confirm) {
if (!options.wait) return renderAsyncSentinelJob(state, "image", "build", options.timeoutSeconds); if (!options.wait) return renderAsyncSentinelJob(state, "image", "build", options.timeoutSeconds);
return runSentinelImageBuildConfirmed(state, options); return runSentinelImageBuildConfirmed(state, options);
@@ -192,10 +192,10 @@ function runSentinelImage(state: SentinelCicdState, options: Extract<WebProbeSen
registry, registry,
blocker: null, blocker: null,
next: { next: {
status: `bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, status: `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`,
dryRun: `bun scripts/cli.ts hwlab nodes web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`, dryRun: `bun scripts/cli.ts web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`,
confirm: `bun scripts/cli.ts hwlab nodes web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, confirm: `bun scripts/cli.ts web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`,
controlPlanePlan: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`, controlPlanePlan: `bun scripts/cli.ts web-probe sentinel control-plane plan --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`,
}, },
valuesRedacted: true, valuesRedacted: true,
}; };
@@ -203,7 +203,7 @@ function runSentinelImage(state: SentinelCicdState, options: Extract<WebProbeSen
} }
function runSentinelControlPlane(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "control-plane" }>): RenderedCliResult { function runSentinelControlPlane(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "control-plane" }>): 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"; const mutationAction = options.action === "apply" || options.action === "trigger-current";
if (options.confirm && mutationAction) { if (options.confirm && mutationAction) {
if (!options.wait) return renderAsyncSentinelJob(state, "control-plane", options.action, options.timeoutSeconds); 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<WebProbeSentinelOptions, { kind: "image" }>): RenderedCliResult { function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "image" }>): 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 publish = runSentinelPublishJob(state, false, options.timeoutSeconds);
const registry = probeImageRegistry(state, options.timeoutSeconds); const registry = probeImageRegistry(state, options.timeoutSeconds);
const registryReady = record(registry.probe).present === true; const registryReady = record(registry.probe).present === true;
@@ -531,8 +531,8 @@ function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extra
warnings: sentinelElapsedWarnings(record(publish).elapsedMs), warnings: sentinelElapsedWarnings(record(publish).elapsedMs),
blocker: null, blocker: null,
next: { next: {
status: `bun scripts/cli.ts hwlab nodes web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, status: `bun scripts/cli.ts 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`, controlPlaneTrigger: `bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`,
}, },
valuesRedacted: true, valuesRedacted: true,
}; };
@@ -540,7 +540,7 @@ function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extra
} }
function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "control-plane" }>): RenderedCliResult { function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "control-plane" }>): 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 applyOnly = options.action === "apply";
const publish = applyOnly ? null : runSentinelPublishJob(state, true, options.timeoutSeconds); const publish = applyOnly ? null : runSentinelPublishJob(state, true, options.timeoutSeconds);
const flush = !applyOnly && record(publish).ok === true 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 { function renderAsyncSentinelJob(state: SentinelCicdState, domain: "image" | "control-plane", action: string, timeoutSeconds: number): RenderedCliResult {
const args = domain === "image" 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)] ? ["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", "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 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 = { const result = {
ok: true, ok: true,
command, command,
@@ -1121,17 +1121,17 @@ function controlPlaneNext(state: SentinelCicdState, action: WebProbeSentinelCont
const node = state.spec.nodeId; const node = state.spec.nodeId;
const lane = state.spec.lane; const lane = state.spec.lane;
return { return {
plan: `bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --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 hwlab nodes web-probe sentinel control-plane status --node ${node} --lane ${lane}`, status: `bun scripts/cli.ts 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}`, image: `bun scripts/cli.ts 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`, 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", issue: "https://github.com/pikasTech/unidesk/issues/889",
currentAction: action, currentAction: action,
}; };
} }
function runSentinelMaintenance(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "maintenance" }>): RenderedCliResult { function runSentinelMaintenance(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "maintenance" }>): 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); const serviceHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds);
if (options.action === "status") { if (options.action === "status") {
const maintenance = callSentinelService(state, "GET", "/api/maintenance", null, options.timeoutSeconds); const maintenance = callSentinelService(state, "GET", "/api/maintenance", null, options.timeoutSeconds);
@@ -1206,7 +1206,7 @@ function runSentinelMaintenance(state: SentinelCicdState, options: Extract<WebPr
} }
function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "validate" }>): RenderedCliResult { function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "validate" }>): 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); const initialHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds);
let quickVerify: Record<string, unknown> | null = null; let quickVerify: Record<string, unknown> | null = null;
if (options.quickVerify) { if (options.quickVerify) {
@@ -1271,7 +1271,7 @@ function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbe
} }
function runSentinelReport(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "report" }>): RenderedCliResult { function runSentinelReport(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "report" }>): 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 }); const query = new URLSearchParams({ view: options.view });
if (options.runId !== null) query.set("run", options.runId); if (options.runId !== null) query.set("run", options.runId);
if (options.traceId !== null) query.set("traceId", options.traceId); if (options.traceId !== null) query.set("traceId", options.traceId);
@@ -1284,12 +1284,12 @@ function runSentinelReport(state: SentinelCicdState, options: Extract<WebProbeSe
} }
function renderAsyncP5Job(state: SentinelCicdState, subcommand: readonly string[], timeoutSeconds: number, releaseId: string | null, reason: string | null, quickVerify: boolean): RenderedCliResult { function renderAsyncP5Job(state: SentinelCicdState, subcommand: readonly string[], timeoutSeconds: number, releaseId: string | null, reason: string | null, quickVerify: boolean): RenderedCliResult {
const args = ["hwlab", "nodes", "web-probe", "sentinel", ...subcommand, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)]; const args = ["web-probe", "sentinel", ...subcommand, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)];
if (releaseId !== null) args.push("--release-id", releaseId); if (releaseId !== null) args.push("--release-id", releaseId);
if (reason !== null) args.push("--reason", reason); if (reason !== null) args.push("--reason", reason);
if (quickVerify) args.push("--quick-verify"); if (quickVerify) args.push("--quick-verify");
const job = startJob(`hwlab_nodes_${state.spec.lane}_web_probe_sentinel_${subcommand.join("_")}`, ["bun", "scripts/cli.ts", ...args], `Run HWLAB ${state.spec.lane} web-probe sentinel ${subcommand.join(" ")} for node ${state.spec.nodeId}`); const job = startJob(`hwlab_nodes_${state.spec.lane}_web_probe_sentinel_${subcommand.join("_")}`, ["bun", "scripts/cli.ts", ...args], `Run HWLAB ${state.spec.lane} web-probe sentinel ${subcommand.join(" ")} for node ${state.spec.nodeId}`);
const command = `hwlab nodes web-probe sentinel ${subcommand.join(" ")}`; const command = `web-probe sentinel ${subcommand.join(" ")}`;
return rendered(true, command, renderAsyncJobResult({ return rendered(true, command, renderAsyncJobResult({
ok: true, ok: true,
command, command,
@@ -1318,7 +1318,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
const runId = `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`; const runId = `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
const steps: Record<string, unknown>[] = []; const steps: Record<string, unknown>[] = [];
const startArgs = [ const startArgs = [
"hwlab", "nodes", "web-probe", "observe", "start", "web-probe", "observe", "start",
"--node", state.spec.nodeId, "--node", state.spec.nodeId,
"--lane", state.spec.lane, "--lane", state.spec.lane,
"--target-path", stringAt(scenario, "observeTargetPath"), "--target-path", stringAt(scenario, "observeTargetPath"),
@@ -1360,7 +1360,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
promptSource: prompts.summary, 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 === "selectProvider") args.push("--provider", stringAt(item, "provider"));
if (type === "sendPrompt") { if (type === "sendPrompt") {
args.push("--text", prompts.prompts[promptIndex % prompts.prompts.length] ?? ""); 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 }); steps.push({ phase: "observe-analyze", ok: analysis.ok, result: analysis.result });
const indexEntry = readLocalObserveIndex(observerId); 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)); 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<string, unknown>[] = []; const cleanupSteps: Record<string, unknown>[] = [];
if (input.promptIndex > 0) { if (input.promptIndex > 0) {
const cancel = runChildCli([ const cancel = runChildCli([
"hwlab", "nodes", "web-probe", "observe", "command", input.observerId, "web-probe", "observe", "command", input.observerId,
"--node", state.spec.nodeId, "--node", state.spec.nodeId,
"--lane", state.spec.lane, "--lane", state.spec.lane,
"--type", "cancel", "--type", "cancel",
@@ -1456,7 +1456,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
cleanupSteps.push({ phase: "observe-cancel-after-failure", ok: cancel.ok, result: cancel.result }); cleanupSteps.push({ phase: "observe-cancel-after-failure", ok: cancel.ok, result: cancel.result });
} }
const stop = runChildCli([ const stop = runChildCli([
"hwlab", "nodes", "web-probe", "observe", "stop", input.observerId, "web-probe", "observe", "stop", input.observerId,
"--node", state.spec.nodeId, "--node", state.spec.nodeId,
"--lane", state.spec.lane, "--lane", state.spec.lane,
"--force", "--force",
@@ -1464,7 +1464,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
], 30); ], 30);
cleanupSteps.push({ phase: "observe-stop-after-failure", ok: stop.ok, result: stop.result }); cleanupSteps.push({ phase: "observe-stop-after-failure", ok: stop.ok, result: stop.result });
const analysis = runChildCli([ const analysis = runChildCli([
"hwlab", "nodes", "web-probe", "observe", "analyze", input.observerId, "web-probe", "observe", "analyze", input.observerId,
"--node", state.spec.nodeId, "--node", state.spec.nodeId,
"--lane", state.spec.lane, "--lane", state.spec.lane,
"--command-timeout-seconds", "55", "--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<string, unknown> { function collectObserveView(state: SentinelCicdState, observerId: string, view: "turn-summary" | "trace-frame", turn: number | null, timeoutSeconds: number): Record<string, unknown> {
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)); if (turn !== null) args.push("--turn", String(turn));
const result = runChildCli(args, timeoutSeconds); const result = runChildCli(args, timeoutSeconds);
const payload = cliDataPayload(result.parsed); const payload = cliDataPayload(result.parsed);
@@ -2124,7 +2124,7 @@ function serviceUnavailableBlocker(state: SentinelCicdState): Record<string, unk
code: "sentinel-service-unavailable", code: "sentinel-service-unavailable",
policy: stringAt(state.cicd, "targetValidation.serviceUnavailablePolicy"), policy: stringAt(state.cicd, "targetValidation.serviceUnavailablePolicy"),
reason: "sentinel service must be reachable through k3s internal Service DNS before quick verify can run; no public/fallback path is used.", reason: "sentinel service must be reachable through k3s internal Service DNS before quick verify can run; no public/fallback path is used.",
retry: `bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node ${state.spec.nodeId} --lane ${state.spec.lane}`, retry: `bun scripts/cli.ts web-probe sentinel validate --node ${state.spec.nodeId} --lane ${state.spec.lane}`,
valuesRedacted: true, valuesRedacted: true,
}; };
} }
@@ -2133,11 +2133,11 @@ function sentinelP5Next(state: SentinelCicdState): Record<string, string> {
const node = state.spec.nodeId; const node = state.spec.nodeId;
const lane = state.spec.lane; const lane = state.spec.lane;
return { return {
validate: `bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node ${node} --lane ${lane}`, validate: `bun scripts/cli.ts 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`, quickVerify: `bun scripts/cli.ts 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`, maintenanceStart: `bun scripts/cli.ts 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`, maintenanceStop: `bun scripts/cli.ts 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`, report: `bun scripts/cli.ts web-probe sentinel report --node ${node} --lane ${lane} --view summary`,
}; };
} }
@@ -1,5 +1,5 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. // 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 { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { rootPath } from "./config"; import { rootPath } from "./config";
@@ -167,7 +167,7 @@ const REQUIRED_TARGET_SHAPES: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, R
export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: WebProbeSentinelConfigAction): WebProbeSentinelConfigPlan { export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: WebProbeSentinelConfigAction): WebProbeSentinelConfigPlan {
const sentinel = spec.observability.webProbe?.sentinel; const sentinel = spec.observability.webProbe?.sentinel;
const command = `hwlab nodes web-probe sentinel ${action} --node ${spec.nodeId} --lane ${spec.lane}`; const command = `web-probe sentinel ${action} --node ${spec.nodeId} --lane ${spec.lane}`;
const rootConfigPath = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`; const rootConfigPath = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`;
if (sentinel === undefined) { if (sentinel === undefined) {
return { return {
@@ -398,7 +398,7 @@ function renderWebProbeSentinelConfigPlan(value: WebProbeSentinelConfigPlan): st
]), ]),
]; ];
return [ return [
`hwlab nodes web-probe sentinel ${commandAction(value.command)} (${value.status})`, `web-probe sentinel ${commandAction(value.command)} (${value.status})`,
"", "",
sentinelTable(["NODE", "LANE", "ENABLED", "OK", "ROOT"], [[value.node, value.lane, value.enabled, value.ok, value.rootPath]]), sentinelTable(["NODE", "LANE", "ENABLED", "OK", "ROOT"], [[value.node, value.lane, value.enabled, value.ok, value.rootPath]]),
"", "",
@@ -431,8 +431,8 @@ function renderWebProbeSentinelConfigPlan(value: WebProbeSentinelConfigPlan): st
function sentinelNext(node: string, lane: string): Record<string, string> { function sentinelNext(node: string, lane: string): Record<string, string> {
return { return {
plan: `bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node ${node} --lane ${lane} --dry-run`, plan: `bun scripts/cli.ts 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}`, status: `bun scripts/cli.ts web-probe sentinel status --node ${node} --lane ${lane}`,
}; };
} }
@@ -335,7 +335,7 @@ function serviceHealth(config: WebProbeSentinelServiceConfig, db: Database, sche
checks.analyzer = { checks.analyzer = {
ok: true, ok: true,
source: "existing observe analyze CLI command", 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 <stateDir>`, command: `bun scripts/cli.ts web-probe observe analyze --node ${config.node} --lane ${config.lane} --state-dir <stateDir>`,
}; };
const ok = Object.values(checks).every((check) => check.ok === true); 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 }; 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 = { const start: CommandPlanStep = {
phase: "observe-start", phase: "observe-start",
argv: [ argv: [
"bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "start", "bun", "scripts/cli.ts", "web-probe", "observe", "start",
"--node", config.node, "--node", config.node,
"--lane", config.lane, "--lane", config.lane,
"--target-path", targetPath, "--target-path", targetPath,
@@ -379,14 +379,14 @@ function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario
}; };
const commands = arrayAt(scenario, "commandSequence").map((item) => { const commands = arrayAt(scenario, "commandSequence").map((item) => {
const type = stringAt(item, "type"); const type = stringAt(item, "type");
const argv = ["bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "command", "<observerId>", "--type", type]; const argv = ["bun", "scripts/cli.ts", "web-probe", "observe", "command", "<observerId>", "--type", type];
if (type === "selectProvider") argv.push("--provider", stringAt(item, "provider")); if (type === "selectProvider") argv.push("--provider", stringAt(item, "provider"));
if (type === "sendPrompt") argv.push("--text-stdin"); if (type === "sendPrompt") argv.push("--text-stdin");
return { phase: `observe-command-${type}`, argv, stdinSource: type === "sendPrompt" ? "prompt-source" : "none" } satisfies CommandPlanStep; return { phase: `observe-command-${type}`, argv, stdinSource: type === "sendPrompt" ? "prompt-source" : "none" } satisfies CommandPlanStep;
}); });
const analyze: CommandPlanStep = { const analyze: CommandPlanStep = {
phase: "observe-analyze", phase: "observe-analyze",
argv: ["bun", "scripts/cli.ts", "hwlab", "nodes", "web-probe", "observe", "analyze", "<observerId>"], argv: ["bun", "scripts/cli.ts", "web-probe", "observe", "analyze", "<observerId>"],
stdinSource: "none", stdinSource: "none",
}; };
return [start, ...commands, analyze]; return [start, ...commands, analyze];
+5 -5
View File
@@ -39,8 +39,9 @@ import { nodeRuntimeUnsupportedAction } from "./runtime-common";
import { runNodeEndpointBridge } from "./secret-scripts"; import { runNodeEndpointBridge } from "./secret-scripts";
import { nodeRuntimeGitMirrorRun, nodeRuntimeGitMirrorStatus, nodeScopedFullOutput, withNodeRuntimeGitMirrorRendered } from "./status"; import { nodeRuntimeGitMirrorRun, nodeRuntimeGitMirrorStatus, nodeScopedFullOutput, withNodeRuntimeGitMirrorRendered } from "./status";
import { assertNodeId, positiveIntegerOption, requiredOption, stripOption } from "./utils"; import { assertNodeId, positiveIntegerOption, requiredOption, stripOption } from "./utils";
import { parseNodeWebProbeOptions, rewriteDelegatedNodeResult, startNodeDelegatedJob } from "./web-probe"; import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe";
import { assertKnownOptions, runNodeWebProbe } from "./web-probe-observe"; import { rewriteDelegatedNodeResult, startNodeDelegatedJob } from "./web-probe";
import { assertKnownOptions } from "./web-probe-observe";
export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help"; 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)); return runHwlabTestAccountsCommand(args.slice(1));
} }
if (domain === "web-probe") { if (domain === "web-probe") {
if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeWebProbeHelp(); return legacyHwlabNodeWebProbeUnsupported(args.slice(1));
return runNodeWebProbe(parseNodeWebProbeOptions(args.slice(1)));
} }
if (domain === "observability") { if (domain === "observability") {
if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeObservabilityHelp(); 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)); return runNodeDelegatedDomain(_config, domain, args.slice(1));
} }
if (domain !== "secret") { 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)); const options = parseSecretOptions(args.slice(1));
return runNodeSecret(options); return runNodeSecret(options);
+2 -2
View File
@@ -248,7 +248,7 @@ export function nodeObservabilityPerformanceSummary(options: NodeObservabilityOp
degradedReason: material.error ?? "web-login-secret-unavailable", degradedReason: material.error ?? "web-login-secret-unavailable",
next: { next: {
secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name bootstrap-admin`, 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 ? report.error
: result.exitCode === 0 ? "web-performance-summary-not-ready" : "web-performance-summary-probe-failed", : result.exitCode === 0 ? "web-performance-summary-not-ready" : "web-performance-summary-probe-failed",
next: { 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}`, 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`, apiMetrics: `bun scripts/cli.ts hwlab nodes observability status --node ${options.node} --lane ${options.lane} --full`,
}, },
+3 -3
View File
@@ -385,7 +385,7 @@ export function nodeRuntimePublicProbeDiagnostic(publicReady: boolean, targetHos
affectsUserEntry: true, affectsUserEntry: true,
targetHostReady: targetReady, targetHostReady: targetReady,
message: "control-plane public probe failed; treat this as a public endpoint readiness failure before using web-probe closeout evidence.", 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) { if (targetHost.probeAvailable !== true) {
@@ -542,7 +542,7 @@ export function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, un
plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`, plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`,
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm`, triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm`,
gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`, gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`,
webProbe: `bun scripts/cli.ts hwlab nodes web-probe run --node ${scoped.node} --lane ${scoped.lane}`, webProbe: `bun scripts/cli.ts web-probe run --node ${scoped.node} --lane ${scoped.lane}`,
}, },
}; };
} }
@@ -572,7 +572,7 @@ export function nodeRuntimeStatusNextAction(status: Record<string, unknown>, 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`; 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") { 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") { if (reason === "git-mirror-pending-flush") {
return `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`; return `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`;
+22 -22
View File
@@ -84,7 +84,7 @@ export function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserve
export function withWebObserveAnalyzeRendered(value: Record<string, unknown>): RenderedCliResult { export function withWebObserveAnalyzeRendered(value: Record<string, unknown>): RenderedCliResult {
return { return {
ok: value.ok !== false, 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", contentType: "text/plain",
renderedText: renderWebObserveAnalyzeTable(value), renderedText: renderWebObserveAnalyzeTable(value),
}; };
@@ -321,7 +321,7 @@ export function renderWebObserveAnalyzeTable(value: Record<string, unknown>): st
webObserveShort(webObserveArray(item.slowSamples).map((sample) => webObserveText(record(sample)?.otelTraceId)).filter((text) => text !== "-").join(",") || "-", 36), webObserveShort(webObserveArray(item.slowSamples).map((sample) => webObserveText(record(sample)?.otelTraceId)).filter((text) => text !== "-").join(",") || "-", 36),
]); ]);
const lines = [ const lines = [
`hwlab nodes web-probe observe analyze (${webObserveText(value.status)})`, `web-probe observe analyze (${webObserveText(value.status)})`,
"", "",
...(value.ok === false ? [ ...(value.ok === false ? [
"Blocked detail:", "Blocked detail:",
@@ -916,7 +916,7 @@ export function upsertWebObserveIndexEntry(entry: WebObserveIndexEntry): Record<
ok: true, ok: true,
id: entry.id, id: entry.id,
path, 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, valuesRedacted: true,
}; };
} catch (error) { } catch (error) {
@@ -925,7 +925,7 @@ export function upsertWebObserveIndexEntry(entry: WebObserveIndexEntry): Record<
id: entry.id, id: entry.id,
path, path,
error: error instanceof Error ? error.message : String(error), 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, valuesRedacted: true,
}; };
} }
@@ -964,16 +964,16 @@ export function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOpti
export function webObserveCommandLabel(action: NodeWebProbeObserveAction, options: Pick<NodeWebProbeObserveOptions, "id" | "jobId" | "node" | "lane">): string { export function webObserveCommandLabel(action: NodeWebProbeObserveAction, options: Pick<NodeWebProbeObserveOptions, "id" | "jobId" | "node" | "lane">): string {
const id = webObserveIdFromOptions(options); const id = webObserveIdFromOptions(options);
return id === null return id === null
? `hwlab nodes web-probe observe ${action} --node ${options.node} --lane ${options.lane}` ? `web-probe observe ${action} --node ${options.node} --lane ${options.lane}`
: `hwlab nodes web-probe observe ${action} ${id}`; : `web-probe observe ${action} ${id}`;
} }
export function webObserveNextCommands(id: string): Record<string, string> { export function webObserveNextCommands(id: string): Record<string, string> {
return { return {
status: `bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, status: `bun scripts/cli.ts web-probe observe status ${id}`,
command: `bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, command: `bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`,
stop: `bun scripts/cli.ts hwlab nodes web-probe observe stop ${id}`, stop: `bun scripts/cli.ts web-probe observe stop ${id}`,
analyze: `bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, analyze: `bun scripts/cli.ts web-probe observe analyze ${id}`,
}; };
} }
@@ -1058,7 +1058,7 @@ export function renderWebProbeRunResult(result: Record<string, unknown>): Record
"", "",
"NEXT", "NEXT",
` report: ${reportPath}`, ` 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"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -1116,10 +1116,10 @@ export function renderWebObserveStartResult(result: Record<string, unknown>): Re
...blockedRows, ...blockedRows,
"", "",
"NEXT", "NEXT",
` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, ` status: bun scripts/cli.ts web-probe observe status ${id}`,
` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`,
` command: bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, ` command: bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`,
` stop: bun scripts/cli.ts hwlab nodes web-probe observe stop ${id}`, ` stop: bun scripts/cli.ts web-probe observe stop ${id}`,
].join("\n"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -1174,9 +1174,9 @@ export function renderWebObserveStatusResult(result: Record<string, unknown>): R
...resultSection, ...resultSection,
"", "",
"NEXT", "NEXT",
` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, ` status: bun scripts/cli.ts web-probe observe status ${id}`,
` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`,
` command: bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`, ` command: bun scripts/cli.ts web-probe observe command ${id} --type mark --label checkpoint`,
].join("\n"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -1203,8 +1203,8 @@ export function renderWebObserveCommandResult(result: Record<string, unknown>):
), ),
"", "",
"NEXT", "NEXT",
` status: bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, ` status: bun scripts/cli.ts web-probe observe status ${id}`,
` analyze: bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, ` analyze: bun scripts/cli.ts web-probe observe analyze ${id}`,
].join("\n"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -1396,7 +1396,7 @@ export function renderWebObserveAnalyzeResult(result: Record<string, unknown>):
` md: ${analysis.reportMdPath ?? "-"}`, ` md: ${analysis.reportMdPath ?? "-"}`,
"", "",
"NEXT", "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"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -1464,7 +1464,7 @@ export function renderWebProbeScriptResult(result: Record<string, unknown>): Rec
"", "",
"NEXT", "NEXT",
` report: ${reportLoad.path ?? probe.reportPath ?? "-"}`, ` 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"); ].join("\n");
return withWebObserveRendered(result, renderedText); return withWebObserveRendered(result, renderedText);
} }
@@ -79,7 +79,7 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
const commandsFailed=commandSummary('failed'); 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 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}; 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)}`; `)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`;
} }
@@ -539,7 +539,7 @@ export function runNodeWebProbeScript(
return renderWebProbeScriptResult({ return renderWebProbeScriptResult({
ok: passed, ok: passed,
status: passed ? "pass" : "blocked", 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, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, workspace: spec.workspace,
+9 -9
View File
@@ -434,8 +434,8 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
ok: false, ok: false,
status: "blocked", status: "blocked",
command: options.action === "observe" command: options.action === "observe"
? `hwlab nodes web-probe observe ${options.observeAction} --node ${options.node} --lane ${options.lane}` ? `web-probe observe ${options.observeAction} --node ${options.node} --lane ${options.lane}`
: `hwlab nodes web-probe ${options.action} --node ${options.node} --lane ${options.lane}`, : `web-probe ${options.action} --node ${options.node} --lane ${options.lane}`,
node: options.node, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, workspace: spec.workspace,
@@ -466,7 +466,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
return renderWebProbeRunResult({ return renderWebProbeRunResult({
ok: passed, ok: passed,
status: passed ? "pass" : "blocked", status: passed ? "pass" : "blocked",
command: `hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`, command: `web-probe run --node ${options.node} --lane ${options.lane}`,
node: options.node, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, workspace: spec.workspace,
@@ -527,7 +527,7 @@ export function runNodeWebProbeAsync(
return renderWebProbeRunResult({ return renderWebProbeRunResult({
ok: false, ok: false,
status: "blocked", status: "blocked",
command: `hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`, command: `web-probe run --node ${options.node} --lane ${options.lane}`,
node: options.node, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, workspace: spec.workspace,
@@ -565,7 +565,7 @@ export function runNodeWebProbeAsync(
return renderWebProbeRunResult({ return renderWebProbeRunResult({
ok: passed, ok: passed,
status: passed ? "pass" : "blocked", status: passed ? "pass" : "blocked",
command: `hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`, command: `web-probe run --node ${options.node} --lane ${options.lane}`,
node: options.node, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, workspace: spec.workspace,
@@ -925,7 +925,7 @@ export function runNodeWebProbeObserveStart(
"pid=$!", "pid=$!",
"printf '%s\\n' \"$pid\" >\"$state_dir/pid\"", "printf '%s\\n' \"$pid\" >\"$state_dir/pid\"",
"sleep 1", "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"); ].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const started = parseJsonObject(result.stdout); const started = parseJsonObject(result.stdout);
@@ -948,7 +948,7 @@ export function runNodeWebProbeObserveStart(
return renderWebObserveStartResult({ return renderWebObserveStartResult({
ok: result.exitCode === 0 && started?.ok === true, ok: result.exitCode === 0 && started?.ok === true,
status: result.exitCode === 0 && started?.ok === true ? "started" : "blocked", 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, node: options.node,
lane: options.lane, lane: options.lane,
workspace: spec.workspace, 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", : "observe analyze failed before producing a compact report; inspect analyzer stdout/stderr artifacts under the observer analysis directory",
}], }],
next: { 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, 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 hwlab nodes web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stderr.log` : 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,
}, },
valuesRedacted: true, valuesRedacted: true,
+32
View File
@@ -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<string, unknown> {
return hwlabNodeWebProbeHelp();
}
export async function runWebProbeCommand(_config: Config, args: string[]): Promise<Record<string, unknown> | 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<string, unknown> {
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,
};
}