From 27f6e4812a3d14f3ce3f63e7b714c20f5fd5c6ea Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 11 Jun 2026 03:31:53 +0000 Subject: [PATCH] docs: record sub2api timeout closeout and agentrun cli updates --- .agents/skills/unidesk-code-queue/SKILL.md | 92 +- .agents/skills/unidesk-sub2api/SKILL.md | 4 +- docs/reference/agentrun.md | 8 +- docs/reference/cli.md | 18 +- docs/reference/platform-infra.md | 2 +- scripts/agentrun-cli-contract-test.ts | 49 +- scripts/cli.ts | 27 +- scripts/src/agentrun.ts | 1001 +++++++++++++++++++- scripts/src/help.ts | 54 +- scripts/src/output.ts | 19 + 10 files changed, 1100 insertions(+), 174 deletions(-) diff --git a/.agents/skills/unidesk-code-queue/SKILL.md b/.agents/skills/unidesk-code-queue/SKILL.md index 273b9e29..710bd7ab 100644 --- a/.agents/skills/unidesk-code-queue/SKILL.md +++ b/.agents/skills/unidesk-code-queue/SKILL.md @@ -1,11 +1,11 @@ --- name: unidesk-code-queue -description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy `codex` 子命令只保留历史只读/残留停止/prompt-lint;新任务提交、Aipod/Artificer 派单、steer、resume、queue mutation、session trace/output/read/cancel、run/command/runner 状态 drill-down 和 HWLAB Code Agent/CaseRun follow-up 必须使用 `agentrun queue|runs|commands|runner|sessions|aipod-specs`。用户提到 codex、Code Queue、submit、steer、resume、tasks、unread、code-queue、aipod、Artificer、HWLAB Code Agent 时使用。 +description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy `codex` 子命令只保留历史只读/残留停止/prompt-lint;新任务提交、Aipod/Artificer 派单、steer/send、events/logs/result、ack/cancel、run/command/runner 状态 drill-down 和 HWLAB Code Agent/CaseRun follow-up 必须使用 `agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send` 资源原语。用户提到 codex、Code Queue、submit、steer、resume、tasks、unread、code-queue、aipod、Artificer、HWLAB Code Agent 时使用。 --- # UniDesk Code Queue / AgentRun CLI -旧 Code Queue 已冻结新任务和写入口。`bun scripts/cli.ts codex ...` 现在只作为历史归档、只读排障、残留任务停止和 prompt-lint 入口;新的指挥官派单、Aipod/Artificer 执行、trace/output、read/cancel、steer/reuse 必须走 AgentRun Queue/Runs/Commands/Runner/Sessions/AipodSpec,并按 cli-spec 渐进披露。 +旧 Code Queue 已冻结新任务和写入口。`bun scripts/cli.ts codex ...` 现在只作为历史归档、只读排障、残留任务停止和 prompt-lint 入口;新的指挥官派单、Aipod/Artificer 执行、events/logs/result、ack/cancel、steer/send 必须走 AgentRun 资源原语,并按 cli-spec 渐进披露。默认输出是低噪声 human 表格/摘要;脚本读取显式使用 `-o json|yaml`,原始官方 bridge 调试显式使用 `--raw`。 **固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts agentrun ...` @@ -15,52 +15,50 @@ description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy ```bash # 查看 AgentRun 指挥官队列 -bun scripts/cli.ts agentrun queue commander --reader-id -bun scripts/cli.ts agentrun queue commander --reader-id --full +bun scripts/cli.ts agentrun get tasks --queue commander --limit 20 +bun scripts/cli.ts agentrun get tasks --queue commander -o wide -# 按已有 CLI 组合查看一个 Aipod/Queue run 生命周期;不要新增 queue lifecycle 命令 -bun scripts/cli.ts agentrun queue show -bun scripts/cli.ts agentrun queue show --full -bun scripts/cli.ts agentrun runs show -bun scripts/cli.ts agentrun runs result --command-id -bun scripts/cli.ts agentrun runs events --after-seq --limit 100 --tail-summary -bun scripts/cli.ts agentrun commands show --run-id -bun scripts/cli.ts agentrun commands result --run-id -bun scripts/cli.ts agentrun runner jobs --run-id --command-id -bun scripts/cli.ts agentrun runner job-status --run-id +# 查看一个 task/run/command/runnerjob/session 生命周期 +bun scripts/cli.ts agentrun describe task/ +bun scripts/cli.ts agentrun events run/ --after-seq --limit 100 +bun scripts/cli.ts agentrun logs session/ --tail 100 +bun scripts/cli.ts agentrun result run/ --command +bun scripts/cli.ts agentrun describe command/ --run +bun scripts/cli.ts agentrun describe runnerjob/ --run # 查看 / 渲染 AipodSpec(Artificer 是默认分布式开发 agent) -bun scripts/cli.ts agentrun aipod-specs list -bun scripts/cli.ts agentrun aipod-specs show Artificer +bun scripts/cli.ts agentrun get aipodspecs +bun scripts/cli.ts agentrun describe aipodspec/Artificer bun scripts/cli.ts agentrun aipod-specs render Artificer --prompt-stdin -# 提交 AgentRun Queue payload -bun scripts/cli.ts agentrun queue submit --json-stdin <<'JSON' -{ - "tenantId": "unidesk", - "projectId": "example", - "queue": "commander", - "title": "任务标题", - "payload": { - "prompt": "任务说明" - } -} -JSON +# 旧 bridge 管理入口仍用于 AipodSpec apply/delete +bun scripts/cli.ts agentrun aipod-specs apply --yaml-stdin --dry-run + +# 提交 AgentRun task manifest +bun scripts/cli.ts agentrun apply -f - --dry-run <<'YAML' +kind: Task +spec: + tenantId: unidesk + projectId: example + queue: commander + title: 任务标题 + payload: + prompt: 任务说明 +YAML # 用 AipodSpec 提交,优先用于新任务和 Artificer -bun scripts/cli.ts agentrun queue submit --aipod Artificer \ +bun scripts/cli.ts agentrun create task --aipod Artificer \ --prompt-stdin --idempotency-key # 查看/控制 AgentRun session -bun scripts/cli.ts agentrun sessions trace -bun scripts/cli.ts agentrun sessions output -bun scripts/cli.ts agentrun sessions read -bun scripts/cli.ts agentrun sessions turn --aipod Artificer --prompt-stdin -bun scripts/cli.ts agentrun sessions steer --prompt-stdin -bun scripts/cli.ts agentrun sessions cancel +bun scripts/cli.ts agentrun logs session/ --tail 100 +bun scripts/cli.ts agentrun ack session/ +bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin +bun scripts/cli.ts agentrun steer session/ --prompt-stdin +bun scripts/cli.ts agentrun cancel session/ --reason --dry-run ``` -日常一次性 JSON、prompt 和 runner JSON 输入优先使用 quoted heredoc/stdin:`--json-stdin`、`--prompt-stdin`、`--runner-json-stdin` 或 `--*-file -`。UniDesk bridge 会把 stdin 直通 G14 `/root/agentrun-v01` 官方 `./scripts/agentrun --manager-url auto` CLI,不先落 dump 文件;`--json-file`、`--prompt-file` 和 `--runner-json-file` 只用于已审阅且可复用的受控文件。它不是旧 Code Queue adapter,不双写,也不迁移旧历史。 +日常 task manifest 优先使用 YAML heredoc:`agentrun apply -f -`;单 prompt 派单优先 `agentrun create task --aipod Artificer --prompt-stdin`。UniDesk bridge 会把 stdin 直通 G14 `/root/agentrun-v01` 官方 `./scripts/agentrun --manager-url auto` CLI,不先落 dump 文件;`--json-file`、`--prompt-file` 和 `--runner-json-file` 只作为旧 bridge 兼容入口用于已审阅且可复用的受控文件。它不是旧 Code Queue adapter,不双写,也不迁移旧历史。 `AipodSpec` 是 AgentRun v0.1 的声明式 agent 装配:模型 profile、gitbundle、skills/tools、SecretRef 和 tool credential 都从 YAML 规格渲染。`Artificer` 默认用于 UniDesk 分布式开发任务,使用 `sub2api` provider、`gpt-5.5`、`reasoningEffort=xhigh`,并通过 SecretRef 注入 GitHub PR token、GitHub SSH 和 UniDesk SSH 透传能力。更新规格时使用 `agentrun aipod-specs apply --yaml-stdin --dry-run` 先看计划,确认后再去掉 `--dry-run`;不得把 API key、SSH key 或 token 写入 prompt、payload、YAML 或 issue。 @@ -68,20 +66,20 @@ AgentRun Queue payload 需要 runner 内使用 UniDesk SSH 透传时,只通过 ## Queue 渐进披露 -AgentRun queue 生命周期不是一个单独的 `queue lifecycle` 命令,而是一组现有 CLI 的渐进披露组合: +AgentRun queue 生命周期不是一个单独的 `queue lifecycle` 命令,而是一组资源原语的渐进披露组合: -1. 默认总览用 `queue commander --reader-id `,只看 active/unread/terminal 摘要和少量任务行。 -2. 单任务用 `queue show `,读取 `latestAttempt.runId`、`commandId`、`runnerJobId`、`sessionId/sessionPath`、Aipod metadata 和 `pollCommands`。 -3. Run 级状态用 `runs show ` 和 `runs result --command-id `,判断 claimed、lease、stale、terminalClassification、failureKind、provider interruption、timeoutBudget 和 recoveryActions。 -4. Command 级状态用 `commands show --run-id ` 和 `commands result --run-id `,确认 command state、ack、terminal status 和结果摘要。 -5. Runner job 只读状态用 `runner jobs --run-id --command-id ` 和 `runner job-status --run-id `,确认 env image reuse、jobName、namespace、phase、exitCode、retention 和 `valuesPrinted=false`。不要为了这些字段手动调用 `trans G14:k3s kubectl ...`。 -6. Session trace/output 只在 `queue show` 或 result 里有实际 `sessionId` 时使用 `sessions trace|output|read|steer|cancel`;`sessionRef=null` 时不要猜 session 命令。 +1. 默认总览用 `get tasks --queue commander --limit 20`,只看 task state、queue/lane、run/cmd/rjob/session ref、age 和 attention。 +2. 单任务用 `describe task/`,读取 `latestAttempt.runId`、`commandId`、`runnerJobId`、`sessionId/sessionPath` 和少量 `Next:`。 +3. Run 级状态用 `events run/` 和 `result run/ --command `,判断 terminalClassification、failureKind、provider interruption、timeoutBudget 和 recoveryActions。 +4. Command 级状态用 `describe command/ --run ` 和 `result command/ --run `,确认 command state、ack、terminal status 和结果摘要。 +5. Runner job 只读状态用 `describe runnerjob/ --run `,确认 env image reuse、jobName、namespace、phase、exitCode、retention 和 `valuesPrinted=false`。不要为了这些字段手动调用 `trans G14:k3s kubectl ...`。 +6. Session trace/output 只在 `describe task` 或 result 里有实际 `sessionId` 时使用 `logs|ack|steer|send|cancel session/`;`sessionRef=null` 时不要猜 session 命令。 -默认视图必须低噪声,`--full` 展开结构化详情,`--raw` 才保留原始响应;命令返回里的 drill-down 应优先是 `bun scripts/cli.ts agentrun ...`,不得把人工 k8s 查询作为日常下一步。 +默认视图必须低噪声且不是 JSON envelope,`-o json|yaml` 才输出稳定机器结构,`--raw` 才保留官方 AgentRun bridge 原始响应;命令返回里的下一步应优先是 `bun scripts/cli.ts agentrun ...` 资源原语,不得把人工 k8s 查询作为日常下一步。 ## HWLAB Code Agent 入口整合 -HWLAB Code Agent / CaseRun follow-up 的日常派单也归入 AgentRun Queue/Sessions:新任务用 `queue submit --aipod Artificer` 或包含 HWLAB gitbundle 的 `queue submit --json-stdin`;运行中纠偏用 `sessions steer` 或 `sessions turn --aipod Artificer`。需要验证 HWLAB Web/Cloud API 原入口时,仍按 `$hwlab-code-agent` 使用 G14 `/root/hwlab-v02` 的 `hwlab-cli client agent ...` 拉取同一 trace/result/inspect;不要回到旧 `codex submit/resume/steer`。 +HWLAB Code Agent / CaseRun follow-up 的日常派单也归入 AgentRun 资源原语:新任务用 `create task --aipod Artificer` 或包含 HWLAB gitbundle 的 `apply -f -`;运行中纠偏用 `steer session/` 或 `send session/ --aipod Artificer`。需要验证 HWLAB Web/Cloud API 原入口时,仍按 `$hwlab-code-agent` 使用 G14 `/root/hwlab-v02` 的 `hwlab-cli client agent ...` 拉取同一 trace/result/inspect;不要回到旧 `codex submit/resume/steer`。 --- @@ -111,7 +109,7 @@ bun scripts/cli.ts codex move --queue bun scripts/cli.ts codex tasks --view commander [--limit N] ``` -返回旧 Code Queue 历史/残留任务的有界 action map:active runners 计数、少量 active item、queued/retry_wait 计数、terminal-unread 总数、关键风险计数、分类计数和 drill-down 命令。新任务队列状态用 `agentrun queue commander`。 +返回旧 Code Queue 历史/残留任务的有界 action map:active runners 计数、少量 active item、queued/retry_wait 计数、terminal-unread 总数、关键风险计数、分类计数和 drill-down 命令。新任务队列状态用 `agentrun get tasks --queue commander`。 ### Supervisor(分区视图) @@ -189,7 +187,7 @@ bun scripts/cli.ts codex interrupt bun scripts/cli.ts codex cancel ``` -仅用于停止旧 Code Queue 残留任务;新 AgentRun session 使用 `bun scripts/cli.ts agentrun sessions cancel `。 +仅用于停止旧 Code Queue 残留任务;新 AgentRun session 使用 `bun scripts/cli.ts agentrun cancel session/`。 --- diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index 74a1c6bc..8eb98eb7 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -113,7 +113,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm - 由 `publicExposure` YAML 控制。默认公共端是 `publicBaseUrl`,master 本地消费端是 `masterBaseUrl`。 - `expose --confirm` 只为 YAML 指定的 `remotePort` 补 master `frps` allow port,并在 G14 创建/更新 `sub2api-frpc`。 -- master Caddy site 也由 `publicExposure.masterCaddy` 渲染;`responseHeaderTimeoutSeconds` 必须足够覆盖 Codex `/responses/compact` 长请求,避免 Caddy 先返回 504 而 Sub2API 后台实际稍后成功。 +- master Caddy site 也由 `publicExposure.masterCaddy` 渲染;`responseHeaderTimeoutSeconds` 必须足够覆盖 Codex `/responses/compact` 长请求,避免 Caddy 先返回 504 而 Sub2API 后台实际稍后成功。具体数值只改 `config/platform-infra/sub2api-codex-pool.yaml`,修改后跑 `codex-pool expose --confirm`,再核对 Caddyfile 中渲染出的 `response_header_timeout`。 - 同一个 FRP TCP 入口同时暴露 OpenAI-compatible API 和 Sub2API 管理 UI `/login`。不要另开第二个管理端口,除非 YAML 明确声明新的暴露决策。 - Sub2API Kubernetes Service 继续保持 ClusterIP。 @@ -152,7 +152,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm - pool key 401:跑 `codex-pool sync --confirm` 重建 Sub2API key 与 k3s Secret 绑定,再跑 `codex-pool validate`。 - 运行中过去的验证探针残留:只用 `codex-pool cleanup-probes --confirm` 清理 `unidesk-probe-*` 临时资源;不要把真实 managed account 删除当作探针清理或可用性恢复。 - FRP 不通:先看 `codex-pool expose --confirm` 输出的 `masterFrps`、`masterCaddy`、`sub2api-frpc` 和 public 401 probe;需要低层证据时只用 `trans G14:k3s` 做 bounded 查询。 -- `/responses/compact` 约 30 秒后返回 504 但 Sub2API 日志稍后记录 `codex.remote_compact.succeeded` 时,优先检查 master Caddy `response_header_timeout` 是否由 YAML `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` 渲染,修正后跑 `codex-pool expose --confirm`;这类边缘代理超时不会触发 Sub2API 账号级临时下线。 +- `/responses/compact` 在接近 master Caddy `response_header_timeout` 的固定时长后返回 504,或 Sub2API 日志稍后记录 `codex.remote_compact.succeeded` 时,优先检查 master Caddy `response_header_timeout` 是否由 YAML `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` 渲染,修正后跑 `codex-pool expose --confirm`;这类边缘代理超时不会触发 Sub2API 账号级临时下线。reload 前已经在途的 compact 请求仍可能按旧 timeout 结束,判断修复是否生效时只看 reload 之后新发起的请求。 - default profile 递归:检查 YAML default entry 是否使用 `*.pre-sub2api` 备份文件;必要时恢复备份后重新 `configure-local --confirm`。 - 上游需要 WebSocket v2:先做 direct Codex WSv2 probe;通过后才给该 profile 配 `openaiResponsesWebSocketsV2Mode: ctx_pool|passthrough` 并跑 `sync --confirm`;把它当 capability candidate,容量仍以 YAML 中的 `capacity` 或默认值为准。 - Codex 启动 WebSocket 回退:用原入口 Codex smoke 复现,再用 bounded Sub2API 日志确认 account;对 WS handshake 4xx/5xx、`openai.websocket_account_select_failed` 或 close-before-`response.completed` 的账号关闭 YAML WSv2 能力后同步。若没有剩余 WSv2-capable account,把 `localCodex.supportsWebSockets` 和 `localCodex.responsesWebSocketsV2` 一起关掉,不把临时可用性推断写成调度配置。 diff --git a/docs/reference/agentrun.md b/docs/reference/agentrun.md index 442ceba8..e8cf6772 100644 --- a/docs/reference/agentrun.md +++ b/docs/reference/agentrun.md @@ -81,7 +81,7 @@ bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --con `cleanup-runs` 是 AgentRun `v0.1` 完成态 CI workspace retention 入口,只清理 `agentrun-ci` namespace 中超过 `--min-age-minutes` 的 `agentrun-v01-ci-*` PipelineRun,通过 Tekton ownerRef 释放临时 workspace PVC。dry-run 必须披露候选 PipelineRun、owned PVC、active mount 保护、local-path 实际估算 bytes 和 confirm 命令。默认保护最新完成的 PipelineRun,保留当前 CI/CD 状态证据。`cleanup-released-pvs` 是二次回收入口,只处理 `agentrun-ci`、`local-path`、`Delete` reclaim policy 的 `Released` PV;它不触碰 `agentrun-v01` runtime namespace、业务 PVC、Secret、registry storage 或 GitOps desired state。磁盘治理和 G14 safe-stop 规则见 `docs/reference/gc.md`。 -涉及 AgentRun runner egress、`transientEnv` 或 Secret 不泄露的 closeout,必须用真实 `queue dispatch`、`sessions turn` 或 `runner-jobs` 路径创建 `agentrun-v01` runner Job,再检查 runner job response、event/trace 和 Kubernetes Pod spec。通过证据应显示 proxy env 是否存在、`NO_PROXY` 是否包含 `hyueapi.com`/`.hyueapi.com`、短期 `HWLAB_API_KEY` 等 `transientEnv` 是否通过 per-job Secret 的 `valueFrom.secretKeyRef` 注入,以及 response/event 只输出 env name、Secret metadata 和 `valuesPrinted=false`。不得在 issue、trace 或 Pod spec 摘要中输出 Secret value。AgentRun 内部 SecretRef 合同以 AgentRun 仓库 `docs/reference/spec-v01-secret-distribution.md` 和 `docs/reference/spec-v01-runtime-assembly.md` 为权威;UniDesk 只记录验证入口和跨仓库归因。 +涉及 AgentRun runner egress、`transientEnv` 或 Secret 不泄露的 closeout,必须用真实 `create/apply/send` 资源原语触发 `agentrun-v01` runner Job,再通过 `describe runnerjob/...`、`events run/...`、`logs session/...` 或必要的兼容 bridge 检查 runner job response、event/trace 和 Kubernetes Pod spec。通过证据应显示 proxy env 是否存在、`NO_PROXY` 是否包含 `hyueapi.com`/`.hyueapi.com`、短期 `HWLAB_API_KEY` 等 `transientEnv` 是否通过 per-job Secret 的 `valueFrom.secretKeyRef` 注入,以及 response/event 只输出 env name、Secret metadata 和 `valuesPrinted=false`。不得在 issue、trace 或 Pod spec 摘要中输出 Secret value。AgentRun 内部 SecretRef 合同以 AgentRun 仓库 `docs/reference/spec-v01-secret-distribution.md` 和 `docs/reference/spec-v01-runtime-assembly.md` 为权威;UniDesk 只记录验证入口和跨仓库归因。 通过 `g14-provider-egress-proxy.unidesk.svc.cluster.local:18789` 验证 `codeload.github.com` 时,必须同时确认 G14 runtime egress Service 有 ready endpoint。Service/DNS 存在但 Deployment `0/1`、Endpoint 只有 notReady address、Pod `ImagePullBackOff` 或 `ContainerStatusUnknown` 时,问题归为 UniDesk/G14 runtime egress 基础设施;不能把 runner 已注入 proxy env 后的 `connect refused` 归为 AgentRun 业务修复失败,也不能关闭要求“通过受控 proxy 成功访问 codeload”的 issue。 @@ -109,11 +109,11 @@ UniDesk 不能作为以下内容的事实来源: ## AgentRun Queue 与旧 Code Queue 边界 -AgentRun `v0.1` 的指挥官任务面已经按 AgentRun issue #105 完成真实运行面验收,可作为新任务派发、Queue commander、trace/output、steer、turn/reuse、read 和 cancel 的 AgentRun 侧标准路径。长期使用时仍以 AgentRun 仓库自身 SPEC 为能力事实来源;UniDesk 只记录该路径已经通过 G14 `agentrun-v01` 运行面和 `hy` profile + `gpt-5.5` 验证。 +AgentRun `v0.1` 的指挥官任务面已经按 AgentRun issue #105 完成真实运行面验收,可作为新任务派发、commander queue 观察、events/logs/result、steer/send、ack 和 cancel 的 AgentRun 侧标准路径。长期使用时仍以 AgentRun 仓库自身 SPEC 为能力事实来源;UniDesk 只记录该路径已经通过 G14 `agentrun-v01` 运行面和 `hy` profile + `gpt-5.5` 验证。 -UniDesk 指挥官新任务入口固定使用 `bun scripts/cli.ts agentrun queue|sessions`。该入口是 G14 `/root/agentrun-v01` 中官方 `./scripts/agentrun --manager-url auto` CLI 的直接 bridge;日常派单、dispatch、turn 和 steer 优先用 `--json-stdin`、`--prompt-stdin`、`--runner-json-stdin` 或 `--*-file -` 的 quoted heredoc/stdin 形式,stdin 会通过管道直通官方 CLI,不先落 dump 文件。`--json-file`、`--prompt-file` 和 `--runner-json-file` 只用于已审阅且可复用的受控文件,bridge 会将其 materialize 到 G14 临时文件后传给官方 CLI。UniDesk 不实现 AgentRun queue 协议,也不把任务 double-write 回旧 Code Queue。 +UniDesk 指挥官新任务入口固定使用 `bun scripts/cli.ts agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send` 资源原语。该入口是 G14 `/root/agentrun-v01` 中官方 `./scripts/agentrun --manager-url auto` CLI 的低噪声包装;默认 human 输出只显示表格、生命周期摘要和下一步命令,脚本读取显式使用 `-o json|yaml`,官方 bridge 原始响应显式使用 `--raw`。日常派单优先用 `agentrun create task --aipod Artificer --prompt-stdin` 或 `agentrun apply -f -` 的 quoted YAML/JSON heredoc/stdin 形式;旧 bridge 的 `--json-file`、`--prompt-file` 和 `--runner-json-file` 只用于已审阅且可复用的兼容调试。UniDesk 不实现 AgentRun queue 协议,也不把任务 double-write 回旧 Code Queue。 -`agentrun control-plane ...` 与 `agentrun queue|runs|commands|runner|sessions ...` 共用同一 UniDesk SSH capture bridge。主 server 本机可继续使用本地 backend-core broker;AgentRun runner、Artificer 或其他没有本地 Docker / `unidesk-backend-core` 容器的环境会自动改走既有 frontend `/ws/ssh` WebSocket backend,并在输出的 `bridge.capture.backend`、`reason` 和 `localBackendCore` 中披露选择依据。本地 `unidesk-backend-core` 容器不是 runner 环境使用这些 AgentRun CLI 入口的隐式前置条件。若所有 capture backend 都不可用,CLI 必须返回 `failureKind=bridge-execution-environment` 与 `capture-backend-unavailable` 或 `bridge-execution-environment-unavailable`,并给出受控恢复入口;AgentRun 官方 CLI 自身返回的 run/command/schema 错误不得被改写成 bridge 失败。 +`agentrun control-plane ...`、资源原语和兼容 bridge 组共用同一 UniDesk SSH capture bridge。主 server 本机可继续使用本地 backend-core broker;AgentRun runner、Artificer 或其他没有本地 Docker / `unidesk-backend-core` 容器的环境会自动改走既有 frontend `/ws/ssh` WebSocket backend,并在输出的 `bridge.capture.backend`、`reason` 和 `localBackendCore` 中披露选择依据。本地 `unidesk-backend-core` 容器不是 runner 环境使用这些 AgentRun CLI 入口的隐式前置条件。若所有 capture backend 都不可用,CLI 必须返回 `failureKind=bridge-execution-environment` 与 `capture-backend-unavailable` 或 `bridge-execution-environment-unavailable`,并给出受控恢复入口;AgentRun 官方 CLI 自身返回的 run/command/schema 错误不得被改写成 bridge 失败。 AgentRun Queue 任务如果需要调用 UniDesk 维护桥,例如 `trans` / `unidesk-ssh`,长期契约以 AgentRun 仓库 `docs/reference/spec-v01-runtime-assembly.md` 和 `docs/reference/spec-v01-secret-distribution.md` 为准:调用方通过 `executionPolicy.secretScope.toolCredentials[].tool=unidesk-ssh` 请求 `UNIDESK_SSH_CLIENT_TOKEN` SecretRef;非敏感 endpoint 由 runner-job `transientEnv` 显式提供,或由 manager 受控默认值自动补齐。UniDesk bridge 提交 Queue payload 时不得在 prompt、payload 或 `transientEnv` 中携带 token,也不得使用 HWLAB runtime Web 入口冒充 UniDesk frontend。若 dispatcher 已正确请求 `unidesk-ssh` 但 trace 的 `runner-job-created.transientEnv.names` 没有 `UNIDESK_MAIN_SERVER_IP`、`UNIDESK_MAIN_SERVER_HOST` 或 `UNIDESK_FRONTEND_URL`,归为 AgentRun assembly 问题;若 endpoint env 已存在但 route denied/timeout,再按 UniDesk frontend/token scope 或 provider session 排查。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6e68cb8c..51dc36fc 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -50,7 +50,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 - `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev workload manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml` 或 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`,foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guard,core manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/Service,Code Queue dev manifest 必须包含 `code-queue-scheduler-dev`、`code-queue-read-dev`、`code-queue-write-dev`、dev provider egress proxy,以及只读挂载宿主 `/home/ubuntu/.agents/skills` 到容器 `/root/.agents/skills` 的 `skills-dir` volume。加 `--kubectl-dry-run` 时额外以 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 执行 `kubectl apply --dry-run=client --validate=false -f `,仍不 apply 资源;默认 `docker-desktop` kubeconfig 不得作为 D601 dry-run 目标。 - `dev-env prewarm-images [--image image] [--provider-id D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]` 创建异步 job,通过 UniDesk SSH 维护桥在 D601 上把开发底座依赖镜像从 Docker 缓存导入原生 k3s containerd。默认镜像是 `postgres:16-alpine` 和 `rancher/mirrored-library-busybox:1.36.1`,用于避免 `postgres-dev` 与 local-path helper pod 卡在外部 registry 拉取。该命令固定验证 `/etc/rancher/k3s/k3s.yaml` 指向的 native k3s 上下文,并输出 `dev_env_containerd_image_ready=...` 作为成功判据;它不 apply manifest、不修改生产 `unidesk` namespace。 - `artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service` 管理 D601 host-managed CNCF Distribution registry 的声明、安装、只读检查和 pull-only artifact CD。该 registry 固定为 D601 loopback `127.0.0.1:5000`,由 systemd + Docker Compose 管理,位于 native k3s 故障域外;`deploy-service` 只拉取 CI 已发布的 commit-pinned 镜像、retag/recreate 或导入 native k3s,并做 live commit 验证,不构建 runtime source。`deploy-backend-core` 是 deprecated 兼容名,标准 backend-core prod CD 入口是 `deploy apply --env prod --service backend-core`。长期规则见 `docs/reference/artifact-registry.md`。 -- `commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr` 是 host Codex 指挥官直管微服务 skeleton 入口。当前命令返回 `phase=source-contract`、service/API/state/bridge/prompt/trace/#20/#46/ClaudeQQ 审批边界、.state/commander/ 状态模型、dev 无 daemon smoke contract、dry-run 计划和 GPT-5.5 PR prompt 边界辅助 lint,不接 live bridge、不注入 prompt、不发送 ClaudeQQ。`approval request --dry-run` 会生成 200 字以内中文纯文本 ClaudeQQ 审批草案、`notification-path-unavailable` blocker 和授权后唯一可用的 `bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '' --raw` 命令;不得提示使用本机 ClaudeQQ skill、powershell 或本地 server。`prompt-lint` 支持 `--prompt-file` 与 `--stdin`,输出 `ok`、`missingClauses`、`riskLevel`、`suggestedPatchSnippet` 且不回显完整 prompt;它是 commander 辅助检查,不是业务 PR 门禁;legacy `codex submit` 已冻结,新任务 payload 审查走 AgentRun Queue。`plan`、`smoke` 与 `approval request` 必须带 `--dry-run`;缺少时返回 `error=dry-run-required`。长期规则见 `docs/reference/host-codex-commander.md`。 +- `commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr` 是 host Codex 指挥官直管微服务 skeleton 入口。当前命令返回 `phase=source-contract`、service/API/state/bridge/prompt/trace/#20/#46/ClaudeQQ 审批边界、.state/commander/ 状态模型、dev 无 daemon smoke contract、dry-run 计划和 GPT-5.5 PR prompt 边界辅助 lint,不接 live bridge、不注入 prompt、不发送 ClaudeQQ。`approval request --dry-run` 会生成 200 字以内中文纯文本 ClaudeQQ 审批草案、`notification-path-unavailable` blocker 和授权后唯一可用的 `bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '' --raw` 命令;不得提示使用本机 ClaudeQQ skill、powershell 或本地 server。`prompt-lint` 支持 `--prompt-file` 与 `--stdin`,输出 `ok`、`missingClauses`、`riskLevel`、`suggestedPatchSnippet` 且不回显完整 prompt;它是 commander 辅助检查,不是业务 PR 门禁;legacy `codex submit` 已冻结,新任务 payload 审查走 AgentRun `create/apply` 资源原语。`plan`、`smoke` 与 `approval request` 必须带 `--dry-run`;缺少时返回 `error=dry-run-required`。长期规则见 `docs/reference/host-codex-commander.md`。 - `hwlab g14 retirement status|plan|execute --confirm [--wait]` 是 legacy G14 DEV/PROD 的受控退役入口。`status` 只读报告 `argocd/hwlab-g14-dev`、`argocd/hwlab-g14-prod`、`hwlab-dev`、`hwlab-prod`、bounded legacy resource preview、受保护的 `hwlab-g14-v02`/`hwlab-node-v03` 和 `hwlab-v02`/`hwlab-v03`,以及 `.state/hwlab-g14/legacy-g14-retirement.json` marker。`plan` 是 dry-run,只列出 destructive targets 和 protected targets。`execute --confirm` 删除 legacy Argo Applications 与 legacy namespaces,取消本地 active `hwlab_g14_pr_monitor` job,并写 retirement marker 记录执行证据;`hwlab g14 monitor-prs --lane g14` 按退役合同固定结构化失败并指向 `retirement status` 和 runtime lane monitor `--lane v02|v03`。该入口禁止触碰 v0.2/v0.3 Application、namespace、PipelineRun、Secret、Git mirror 或 FRP desired state。 - `hwlab g14 monitor-prs --lane v02` 是 HWLAB `v0.2` 的 PR -> CI -> CD 自动化入口。它只监控 base=`v0.2` 的 open PR:每轮先用 UniDesk `gh pr preflight` 读取 GitHub CI/checks、mergeability 和冲突状态;pending 时在 PR 下写等待评论,blocked/conflict 时写阻塞评论;ready 时直接用 UniDesk `gh pr merge` 合并,不因为其他 commit 的运行中 PipelineRun 阻塞 merge 或 CI 启动。合并后执行受控 `control-plane trigger-current --lane v02 --confirm --wait`、轮询定点 `control-plane status --lane v02 --source-commit `,必要时执行 `git-mirror flush --confirm --wait`。v0.2 CD 采用 latest-only:旧 PipelineRun 不取消、不等待,但 promotion 写 `v0.2-gitops` 前必须重新确认 source head,stale commit 只能以 superseded/no-op 收口,不能回滚 runtime。不管 CD 成功、superseded、失败或超时,都在原 PR 下用 `gh pr comment create --body-stdin <<'EOF'` 追加语义化状态,正文固定包含起止时间、总耗时、冲突状态、CI/preflight conclusion、source commit、PipelineRun、targetValidation、Argo/webAssets 和 git mirror pendingFlush/githubInSync。评论去重状态写入 `.state/hwlab-g14/v02-pr-comment-signatures.json`,同一状态签名不会重复刷评论;v0.2 monitor 指针使用 `.state/hwlab-g14/latest-v02-monitor-job.json`、`latest-v02-once-job.json`、`latest-v02-dry-run-job.json` 和 `latest-v02-once-dry-run-job.json`,不会覆盖默认 G14 monitor 指针。`--lane v02 --once --dry-run` 只做单轮 preflight/merge/CD/comment plan,不写 GitHub、不触发 CD。 - `hwlab g14 monitor-prs --lane v03` 是 HWLAB `v0.3` 的 PR -> CI -> 自动合并 -> CD 入口。它只监控 base=`v0.3` 的 open PR:每轮先通过 UniDesk `gh pr preflight` 读取 GitHub checks、mergeability 和冲突状态;pending 时只在 PR 下写等待评论;失败 check、preflight blocker 或 conflict 时在 PR 下写阻塞评论,并按标题去重创建或更新 HWLAB failure issue。ready 时通过 UniDesk `gh pr merge` 合并,随后执行 runtime lane `control-plane trigger-current --lane v03 --confirm --wait`,轮询 `control-plane status --lane v03 --source-commit `,判定 PipelineRun `True`、Argo `Synced/Healthy`、`hwlab-v03` runtime workload 可见、20666/20667 public probes 通过,并在必要时执行 `git-mirror flush --lane v03 --confirm --wait`。CD 成功、失败或超时都会在原 PR 下写语义化状态评论;失败和超时同时创建或更新 failure issue,正文必须包含 PR、base/head、commit、PipelineRun、失败阶段、preflight/CD 摘要和下一步 CLI。评论去重状态写入 `.state/hwlab-g14/v03-pr-comment-signatures.json`,monitor 指针使用 `.state/hwlab-g14/latest-v03-monitor-job.json`、`latest-v03-once-job.json`、`latest-v03-dry-run-job.json` 和 `latest-v03-once-dry-run-job.json`。`--lane v03 --once --dry-run` 只做单轮 preflight/merge/CD/comment/issue plan,不写 GitHub、不触发 CD。 @@ -74,7 +74,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 - `trans gh:/owner/repo ...` 把 GitHub issue/PR 映射成只读/受控写入的虚拟文本目录,适合日报、PR 正文和 issue 正文的小补丁维护:`trans gh:/pikasTech/HWLAB ls` 展示 `pr/` 与 `issue/`,`trans gh:/pikasTech/HWLAB/pr ls [--limit N] [--full]` 和 `trans gh:/pikasTech/HWLAB/issue ls [--limit N] [--full]` 展示条目状态、楼层数、正文长度和标题,`trans gh:/pikasTech/HWLAB/pr/507 ls` 展示单个 PR 的一楼正文文件,`trans gh:/pikasTech/HWLAB/505/1 cat|rg|patch-apply` 兼容旧式 issue/PR number route。`patch-apply` 使用 UniDesk 默认 apply-patch v2 的虚拟文件 executor,把正文一楼映射为 `body.md`,写回仍走 `bun scripts/cli.ts gh issue/pr update` 的 guard/concurrency 规则;`rm` 对正文一楼结构化拒绝,避免误删 issue/PR 正文。大正文读取必须展开 UniDesk gh dump 文件,否则 `cat/rg/patch-apply` 会误读为空,这是 `gh:` 虚拟文件接口的 P0 可见性契约。 - `hwlab cd status|audit|preflight|apply --env dev [--dry-run]` 是旧 D601 HWLAB DEV CD 指挥侧 wrapper,仅用于显式 legacy 诊断和迁移对照。默认通过 UniDesk provider `host.ssh` 进入 D601,再调用 HWLAB repo-owned `scripts/dev-cd-apply.mjs`,不内嵌发布 kubectl 逻辑:`status` 汇总固定 CD mirror、Git clean/main/origin-main、`deploy/deploy.json`/artifact catalog/report、D601 native k3s guard 和 CD Lease lock,并用 `scripts/dev-cd-apply.mjs --status --skip-live-verify` 取得 target/promotion 摘要;`audit` 在 k3s/CD 恢复后做只读健康审计,返回有界 JSON 的 blocker 分类、D601 guard/node、SecretRef 存在性、registry 可达性、Lease phase/holder/staleness、deploy.json 与 artifact/workload image 收敛、current Deployment image/revision/rollout、16666/16667 public health commit/readiness 和 DB/runtime durability 摘要;`preflight` 进一步检查必需 SecretRef 对象/键存在性并运行 HWLAB `scripts/dev-cd-apply.mjs --dry-run --skip-live-verify` 受控事务摘要。完整远端 stdout/stderr 写入 D601 `~/.state/unidesk-hwlab-cd//` 和本地 `.state/hwlab-cd//` task dump,stdout 只返回有界摘要。默认 HWLAB CD repo 是 `/home/ubuntu/hwlab_cd`,`/home/ubuntu/hwlab` runner 历史目录不得作为发布真相。wrapper 强制 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并只以这个显式目标作为 gate;显式目标出现 `docker-desktop`、`desktop-control-plane` 或 `127.0.0.1:11700` 信号会结构化拒绝,audit/preflight/apply --dry-run 都必须观察到 node `d601`。真实 apply 只暴露 `scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report` 命令形状并标注 host-commander-only,本 runner 不执行 live apply、rollout、Lease mutation 或 DEV deploy apply。长期规则见 `docs/reference/hwlab.md`。 - `gh auth status [--repo owner/name]` 探测 GitHub 操作前置条件并输出脱敏 JSON:是否存在 `gh` binary、是否存在 `GH_TOKEN`/`GITHUB_TOKEN` 或可用 `gh auth token` fallback、REST API 是否可达、目标 repo 是否可见、issue 是否可读。degraded reason 必须归类为 `missing-binary`、`missing-token`、`auth-failed`、`github-transient`、`network-proxy-failed`、`permission-denied`、`repo-not-found`、`repo-forbidden`、`issue-not-found`、`pr-not-found`、`scope-insufficient`、`validation-failed`、`invalid-response` 或 `unsupported-command`,不得打印 token;失败对象必须包含 `runnerDisposition=infra-blocked|business-failed`,runner 应优先用该字段分流。`github-transient` 表示 GitHub DNS/API 连接在收到 HTTP 状态前失败,输出应带 `retryable=true` 或等价 commander action;这不是缺 token、认证失败、权限不足或 PR 语义失败。 -- `codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` 是派发前的本地 dry-run prompt lint。它只读取 prompt 文本,返回 `dryRun=true`、`mutation=false`、`declaredClass`、`effectiveClass`、`requiredClass`、`dispatchDisposition`、缺失或矛盾项和有界 evidence,不访问 live service、不提交任务、不打印完整 prompt。分级固定为 `read-only`、`live-read`、`live-mutating`;未声明时按 `read-only` 处理。新任务走 AgentRun Queue,指挥官应把 lint 结果纳入 `agentrun queue submit` payload 审查。长期规则见 `docs/reference/code-queue-supervision.md` 的 DEV 测试授权分级。 +- `codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` 是派发前的本地 dry-run prompt lint。它只读取 prompt 文本,返回 `dryRun=true`、`mutation=false`、`declaredClass`、`effectiveClass`、`requiredClass`、`dispatchDisposition`、缺失或矛盾项和有界 evidence,不访问 live service、不提交任务、不打印完整 prompt。分级固定为 `read-only`、`live-read`、`live-mutating`;未声明时按 `read-only` 处理。新任务走 AgentRun `create/apply` 资源原语,指挥官应把 lint 结果纳入 task manifest 或 prompt 审查。长期规则见 `docs/reference/code-queue-supervision.md` 的 DEV 测试授权分级。 - `gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]` 通过 GitHub REST 列出 issue,默认 `state=open`、`limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 是 CLI 返回上限,不等同 GitHub 单页 `per_page`:当 `--limit > 100` 或默认页中混入 PR 时,CLI 必须分页抓取 GitHub REST/Search page,过滤 PR 后再返回 issue,并在输出中披露 `pagination.fetchedPages/rawCount/hasMore`;`hasMore=true` 时只能说明当前有界扫描未穷尽,禁止把它当作“仓库没有更多 issue”。`--search` 使用 GitHub Search Issues API,并自动追加 `repo:/`、`type:issue` 和 state qualifier,用于创建新 issue 前做低摩擦查重;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--label` 是 GitHub REST `labels=label1,label2` 或 Search `label:` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read`、`pr list`)必须结构化失败并指明 `gh issue create/list/stale-close` 才是合法作用域。GitHub issues API 可能混入 PR,CLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full` 在 `gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true`,`output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。 - `gh issue lifecycle`:`--state` 只能作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `gh issue edit` 只写 body/title,**不接受** `--state` 改 open/closed。把 `gh issue update --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close ` / `gh issue reopen `(PR 用 `gh pr close|reopen `),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;需要正文时后续使用返回的 `readCommands` 或 `gh issue view --json body|--full|--raw`。生命周期 close/reopen 的评论推荐用 `--comment-stdin <<'EOF'` 直接写 heredoc/stdin;短单行可用 `--comment`,已有复用文件才用 `--comment-file`。需要附长篇 CLI 验收证据时,先用 `gh issue comment create --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close --comment <短引用>` 关闭。issue 硬删除走 `close`,PR 硬删除走 `close`,两者都没有"delete"语义。 - `gh issue comment create --repo owner/name --body-stdin`、`gh issue comment update|edit --repo owner/name --body-stdin`、`gh issue comment delete --repo owner/name`、`gh issue close --repo owner/name [--comment |--comment-stdin]`、`gh issue reopen --repo owner/name [--comment |--comment-stdin]`、`gh issue update --repo owner/name [--title ...] [--body-stdin]`、`gh issue edit ...`、`gh issue board-row get|update|add|move|delete|upsert --repo owner/name ...` 都接受与 `gh issue view|read`、`gh pr *` 一致的 `owner/repo#number` 位置 shorthand;shorthand 与显式 `--repo` 冲突时结构化失败并把两者都回显到错误对象里,避免静默改写目标 repo。`gh issue view|read`、`gh pr view|read|files|diff|preflight|closeout|comment create|comment update|comment edit|comment delete|close|reopen|merge|edit|update` 已长期支持该 shorthand;comment update/edit/delete 的 `--number` 表示 commentId,不是 issue/PR number。issue 写命令对齐后整个 `gh` 子命令在 shorthand 行为上保持一致,不再需要把 `pikasTech/HWLAB#621` 拆成 `621 --repo pikasTech/HWLAB`。来源:HWLAB #621 CLI 验收 `gh issue comment create pikasTech/HWLAB#621` 摩擦改进。 @@ -93,9 +93,9 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 - `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core` 与 `publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/:` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId`、`sourceCommit`、`sourceRepo`、`dockerfile`、`imageRef`、`tag`、`digest`、`digestRef`),但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`。 - `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run ` 只接受 failed run,从原 run 反查 `scheduleId` 后重触发同一 schedule,并输出 `originalRunId`、`scheduleId`、`newRunId` 和 `observeCommand`。当 backend-core 目标容器缺失或只观察到 verify-only 容器时,schedule/microservice 命令必须以非零退出并返回 `failureKind=target-stack-not-running`、`runnerDisposition=infra-blocked`、`readOnlyCommands` 和 `authorizationRequiredForRecovery`,不得把 Docker 的 `No such container` 当成成功的空历史。 - `codex deploy ` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 -- `agentrun queue|sessions` 是当前指挥官新任务和 AgentRun session 控制入口。UniDesk CLI 通过 G14 `/root/agentrun-v01` 中官方 `./scripts/agentrun --manager-url auto` 执行:`queue commander` 查看指挥官队列,`queue submit --json-stdin` 创建新任务,`queue dispatch --json-stdin` 派发,`queue read/cancel` 标记和取消队列任务;`sessions trace/output/read/steer/cancel` 读取和控制已创建 session。日常一次性 JSON、prompt 和 runner JSON 输入优先用 quoted heredoc/stdin;`--json-file`、`--prompt-file`、`--runner-json-file` 只用于已审阅且可复用的受控文件。本地 bridge 对 stdin 直通官方 AgentRun CLI,不先落 dump 文件;它不是旧 Code Queue adapter,不做双写,也不迁移旧历史。 -- `agentrun control-plane ...` 与 `agentrun queue|runs|commands|runner|sessions ...` 共用 UniDesk SSH capture bridge。主 server 本机可使用本地 backend-core broker;Artificer/AgentRun runner 等没有本地 Docker 或 `unidesk-backend-core` 容器的环境会自动使用 frontend `/ws/ssh` WebSocket backend,并在 `bridge.capture.backend`、`reason`、`localBackendCore` 中披露选择依据。本地 `unidesk-backend-core` 不是 runner 环境的隐式前置条件;若 capture backend 不可用,错误必须归类为 `failureKind=bridge-execution-environment` 并给出受控恢复入口。 -- `codex submit/enqueue`、`codex steer`、`codex resume`、`codex queue create`、`codex queue merge`、`codex move`、旧 Web 提交表单、旧队列管理和旧 workdir 管理是冻结的 legacy Code Queue 写入口。CLI 必须返回 `ok=false`、`frozen=true`、`degradedReason=legacy-code-queue-frozen` 和 AgentRun 替代命令;服务端旧 API 写入口必须返回 410。新任务、steer、trace/output、read 和 cancel 走 AgentRun Queue/Sessions。 +- `agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send` 是当前指挥官新任务和 AgentRun session 控制入口。UniDesk CLI 通过 G14 `/root/agentrun-v01` 中官方 `./scripts/agentrun --manager-url auto` 执行,默认 human 输出只显示表格、生命周期摘要和下一步命令;脚本读取显式使用 `-o json|yaml`,官方 bridge 原始响应显式使用 `--raw`。日常查看用 `get tasks --queue commander`、`describe task/`、`events run/`、`logs session/`、`result run/ --command `;日常写入用 `create task --aipod Artificer --prompt-stdin`、`apply -f -`、`steer/send session/`、`ack/cancel task|session/`。兼容 bridge 组 `queue|runs|commands|runner|sessions|aipod-specs` 只保留为 raw/debug 和尚未包装的低频能力入口。 +- `agentrun control-plane ...`、资源原语和兼容 bridge 组共用 UniDesk SSH capture bridge。主 server 本机可使用本地 backend-core broker;Artificer/AgentRun runner 等没有本地 Docker 或 `unidesk-backend-core` 容器的环境会自动使用 frontend `/ws/ssh` WebSocket backend,并在 `bridge.capture.backend`、`reason`、`localBackendCore` 中披露选择依据。本地 `unidesk-backend-core` 不是 runner 环境的隐式前置条件;若 capture backend 不可用,错误必须归类为 `failureKind=bridge-execution-environment` 并给出受控恢复入口。 +- `codex submit/enqueue`、`codex steer`、`codex resume`、`codex queue create`、`codex queue merge`、`codex move`、旧 Web 提交表单、旧队列管理和旧 workdir 管理是冻结的 legacy Code Queue 写入口。CLI 必须返回 `ok=false`、`frozen=true`、`degradedReason=legacy-code-queue-frozen` 和 AgentRun 替代命令;服务端旧 API 写入口必须返回 410。新任务、steer/send、events/logs/result、ack 和 cancel 走 AgentRun 资源原语。 - 旧 Code Queue 只保留历史归档、只读排障和残留任务停止。`codex task/tasks/output/read/unread/queues` 继续通过 backend-core 私有代理读取旧 PostgreSQL 历史;`codex interrupt|cancel ` 只用于停止旧运行面残留任务。旧 `steer-confirm` 只作为历史 trace confirmation 查询,不是新任务控制入口。 - `codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full|--raw]` 通过稳定 `code-queue` proxy 请求 D601 scheduler `/api/runtime-preflight`,用于 PR 型派单 admission。默认输出是紧凑 commander 视图,显式分出 `schedulerPreflight` 与 `activeRunnerPrCapability`,并附带 `commands` 和 `disclosure`,方便先看 scheduler auth 缺口、再看当前 runner/dev container 的 `gh auth status` 与 `gh pr create --dry-run` 能力;`--full` 或 `--raw` 才展开完整 `preflight`、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测和观测原文。只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。当 auth-broker 配置存在时,`tokenCoverage.source="auth-broker"`、`credentialSource="broker-issued-token"` 且 runner env token 不是成功前提;当仅 env token 存在时,`credentialSource="env-token"` 且 `authBroker.nextAction="use-env-token-until-auth-broker-live"`;两者都缺失时顶层 `ok=false`、`runnerDisposition=infra-blocked`、`degradedReason=auth-broker-needed`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`,并输出 `authBroker.source="broker/auth-broker-needed"`、`capability.source="missing-token"`。该 `auth-missing` 的 scope 是 `scheduler-runner-env`,不能简化成“当前 active runner/dev container 不能创建 PR”;默认视图必须带 `scopeBoundary` 和 `activeRunnerPrCapability`。GitHub DNS/API 连接失败应归类为 `failureKind=github-transient`、`degradedReason=github-dns-api-transient`,并带 `retryable=true`、`commanderAction=retry-backoff-or-keep-running-if-heartbeat-fresh` 和有界 `githubTransient.failedProbes`;调用方应重试/退避,且在任务 heartbeat/trace 新鲜时继续监督,不把它当成 auth 缺失或 PR 语义失败。`prCapability` 是 runner-facing 合同摘要,必须包含目标分支、token/auth 来源、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff、真实 PR 创建需要 commander 授权,以及 guarded `gh pr merge --dry-run` 预检路径;系统 `gh` binary 缺失只进入 `tools.systemGhBinary`,不得误判为 UniDesk REST `gh` CLI 不可用。`--remote` 在 runner-like 环境里不再依赖本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些缺失只作为本地观测证据。若远程控制面可达,则继续走远程控制面结果;若远程控制面不可达,则结构化返回 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,而不是把本地 `backend-core-container-missing` 当作最终阻塞。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token/auth broker、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。`--detail` 仍是有界详细摘要:默认只返回少量 attempt/tool 行、短 prompt/response/stderr/feedback 预览和 omitted/truncated 元数据;需要完整 prompt/response 文本或更多 tool/attempt 细节时再显式加 `--full`、`--tool-limit N`、`--trace` 或 `codex output`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 @@ -107,10 +107,10 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 - `codex dev-ready` 查询 Code Queue `/api/dev-ready` 并返回有界 readiness 摘要,包括工具、Docker、Codex config、SSH 和 `devReady.skills`。`devReady.skills` 只暴露 `UNIDESK_SKILLS_PATH`、是否存在、是否只读、skillCount、`cli-spec` 是否可见和修复建议,不输出宿主 auth/token 文件内容。 - `codex judge --attempt N [--dry-run] [--include-prompt]` 通过 Code Queue 私有代理按指定 attempt 单步复现 judge;这是执行面诊断入口,仍依赖 D601 scheduler/runner 侧的真实 judge builder、MiniMax 调用路径和执行环境。默认会真实调用 MiniMax,`--dry-run` 只返回 prompt/payload 大小、attempt 窗口和重建来源诊断,`--include-prompt` 仅用于本地深度排查。 - `codex steer-confirm --steer-id [--raw]` 是只读 trace confirmation lookup。默认输出 `traceConfirmation.found/accepted/deliveryState/trace.seq/trace.at/promptChars/promptHash` 和 `delivery.status`,不回显 prompt;`--raw` 才附带原始 backend confirmation body。该命令用于处理 stable-proxy abort 后的 `deliveryUnconfirmed`,不要用重复 prompt 代替确认查询。 -- 旧 `codex steer` 已冻结;`codex steer-confirm` 只作为历史 trace confirmation lookup。新运行中纠偏使用 `bun scripts/cli.ts agentrun sessions steer --prompt-stdin`,并用 `sessions trace/output/read` 观察。 +- 旧 `codex steer` 已冻结;`codex steer-confirm` 只作为历史 trace confirmation lookup。新运行中纠偏使用 `bun scripts/cli.ts agentrun steer session/ --prompt-stdin`,并用 `logs session/`、`events run/`、`result run/ --command ` 和 `ack session/` 观察。 - `codex interrupt|cancel ` 通过 Code Queue 私有代理请求中断;running/judging 任务会请求 D601 当前 agent run 停止,queued/retry_wait 任务的取消也必须保持与 WebUI 相同代理路径,返回有界 task 摘要和后续查询命令。任何需要接触 active run 的动作仍属于 D601 执行面。 -- 旧 Code Queue 多队列 lane 现在是归档视图:`codex queues [--full|--all] [--limit N] [--page N|--offset N]` 只读展示历史 queue 摘要、activity、commanderConcurrency、counts 和 execution diagnostics。`queue create`、`queue merge`、`move` 等旧队列写入口冻结并返回 `legacy-code-queue-frozen`;AgentRun 新任务的排队、派发和取消必须使用 `agentrun queue`。 -- 所有旧 `codex` 历史查询、已读和残留 interrupt/cancel 命令必须走与 WebUI 相同的 backend-core 私有代理路径 `/api/microservices/code-queue/proxy/...`。旧 submit/steer/resume/queue mutation/move/workdir mutation 不得绕过冻结;若需要新任务或新 session 控制,使用 AgentRun Queue/Sessions。 +- 旧 Code Queue 多队列 lane 现在是归档视图:`codex queues [--full|--all] [--limit N] [--page N|--offset N]` 只读展示历史 queue 摘要、activity、commanderConcurrency、counts 和 execution diagnostics。`queue create`、`queue merge`、`move` 等旧队列写入口冻结并返回 `legacy-code-queue-frozen`;AgentRun 新任务的排队、派发和取消必须使用 `agentrun create|apply|get|cancel`。 +- 所有旧 `codex` 历史查询、已读和残留 interrupt/cancel 命令必须走与 WebUI 相同的 backend-core 私有代理路径 `/api/microservices/code-queue/proxy/...`。旧 submit/steer/resume/queue mutation/move/workdir mutation 不得绕过冻结;若需要新任务或新 session 控制,使用 AgentRun 资源原语。 - `job list [--limit N] [--include-command]` 与 `job status [--tail-bytes N]` 查询 `.state/jobs/` 文件系统状态,是异步命令的可观测入口。`job list` 默认只返回最新 50 条摘要,并为已知异步工作流返回轻量 `progress.summary` 与后续查询命令;`job status` 默认返回结构化 `progress`、stdout/stderr 末尾 12000 字节、`tailPolicy` 与完整日志路径。已知工作流应从有界日志尾部抽取阶段、关键对象名和下一步命令,避免为了判断当前阶段而手工打开完整 stdout/stderr。`hwlab_g14_v02_trigger_current` 的 progress 必须暴露 trigger 阶段、source commit 和 PipelineRun;`hwlab_g14_v02_pr_monitor` 的 progress 必须暴露 preflight、merge、source-head、cd-trigger、cd-status、git-mirror-flush 和 pr-comment 阶段,以及 PR、source commit、PipelineRun、targetValidation/pendingFlush 摘要;`hwlab_g14_git_mirror_sync|flush` 与 `agentrun_v01_git_mirror_sync|flush` 的 progress 必须暴露 sync/flush 状态、Job 名、pendingFlush 与 fetch/push/total/SSH timing,并给出对应 repo 的 mirror status 命令。 - `debug health`、`debug dispatch` 与 `debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程,只用于开发调试,不写入 `TEST.md` 的正式验收步骤。 - `e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]` 使用 publicHost 派生的公开 production frontend/dev frontend/provider ingress URL,并通过 Docker 内网验证 core API、PostgreSQL、provider self-connection、系统指标曲线、Docker 状态快照、provider.upgrade 预检和 Playwright 前端页面,是交付前的自动化 E2E 门禁;CLI 默认输出 check 状态摘要,完整诊断写入 `resultPath`,日常迭代应优先用 `--only` / `--skip` 跑最小必要集合。 @@ -445,7 +445,7 @@ PATCH `--main-server-ip` 是一个全局前缀,必须放在需要透传的命令同一次调用中,例如 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`。默认传输是公网 frontend:本地 CLI 读取本仓库 `config.json` 中的 frontend 登录账号密码,登录 `http://:/` 获取 HttpOnly session cookie,然后通过 frontend 的 `/api/*` 同源代理访问 backend-core 内网 API;因此计算节点只需要能访问公网 frontend,不需要主 server SSH key,也不需要打开 backend-core REST API 或 PostgreSQL 端口。 -默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task `、`codex tasks`、`codex unread`、`codex queues`、`codex output `、`codex judge --attempt N` 和 `ssh `。`microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary,`microservice health code-queue` 只有显式 `--raw` 或 `--full` 才返回完整健康 body。运行中纠偏已切到 AgentRun `sessions steer`;旧 `codex steer` 属于冻结写入口,不应通过 frontend 远程传输或旧 proxy 绕过。其中 `ssh` 的 remote frontend 传输使用 authenticated frontend `/ws/ssh` WebSocket 代理接入 backend-core SSH bridge,stdout/stderr 按字节流直通到调用端,不经过 `/api/dispatch`、`/api/tasks` 或 task JSON compact;frontend 运行时必须通过 `PROVIDER_TOKEN`/`UNIDESK_PROVIDER_TOKEN` 或 `PROVIDER_TOKEN_FILE`/`UNIDESK_PROVIDER_TOKEN_FILE` 读取 provider token,并且不能把 token 下发给 runner。因此 D601 Code Queue runner 内的 `tran G14 ...` 应与主 server 本机 `trans G14 ...` / `tran G14 ...` 在输出完整性上保持同一语义。非交互单进程命令优先 `trans D601 argv true`;`apply-patch`、stdin script、`py` 和旧 `apply-patch-v1` fallback 也走同一条 `/ws/ssh` 流式通道。交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 +默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task `、`codex tasks`、`codex unread`、`codex queues`、`codex output `、`codex judge --attempt N` 和 `ssh `。`microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary,`microservice health code-queue` 只有显式 `--raw` 或 `--full` 才返回完整健康 body。运行中纠偏已切到 AgentRun `steer session/`;旧 `codex steer` 属于冻结写入口,不应通过 frontend 远程传输或旧 proxy 绕过。其中 `ssh` 的 remote frontend 传输使用 authenticated frontend `/ws/ssh` WebSocket 代理接入 backend-core SSH bridge,stdout/stderr 按字节流直通到调用端,不经过 `/api/dispatch`、`/api/tasks` 或 task JSON compact;frontend 运行时必须通过 `PROVIDER_TOKEN`/`UNIDESK_PROVIDER_TOKEN` 或 `PROVIDER_TOKEN_FILE`/`UNIDESK_PROVIDER_TOKEN_FILE` 读取 provider token,并且不能把 token 下发给 runner。因此 D601 Code Queue runner 内的 `tran G14 ...` 应与主 server 本机 `trans G14 ...` / `tran G14 ...` 在输出完整性上保持同一语义。非交互单进程命令优先 `trans D601 argv true`;`apply-patch`、stdin script、`py` 和旧 `apply-patch-v1` fallback 也走同一条 `/ws/ssh` 流式通道。交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 计算节点可以用该入口测试自身的远程升级闭环,而不需要在计算节点公开 core REST API 或 database。标准顺序是:先运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health` 确认主 server 看到当前 Provider 在线,且该 Provider labels 中 `unideskCapabilities` 包含 `host.ssh`、`hostSshConfigured=true`、`hostSshKeyPresent=true`;再运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch provider.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`;随后再次运行 `debug health` 确认节点重新上线;最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测,不能视为交付完成。 diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index a1b0365d..66cc2256 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -42,7 +42,7 @@ - `profiles.entries[].openaiResponsesWebSocketsV2Mode` is the account-level Responses WebSocket v2 switch for OpenAI-compatible upstreams that require WebSocket transport. Allowed values are `off`, `ctx_pool`, and `passthrough`; omit the field unless that upstream needs it. - `profiles.entries[].upstreamUserAgent` is an optional account-level upstream request User-Agent override. Use it only for upstreams that require a Codex CLI compatible User-Agent; keep the value YAML-controlled and newline-free. - `publicExposure` controls the optional FRP bridge from master server to the G14 ClusterIP service. -- `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` controls the master Caddy `response_header_timeout` for the public Sub2API site. It must be long enough for Codex `/responses/compact` requests; otherwise Caddy can return a client-visible 504 before Sub2API finishes the upstream compact request, and that edge timeout is not an account-level upstream failure that Sub2API can use for temporary-unschedulable failover. +- `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` controls the master Caddy `response_header_timeout` for the public Sub2API site. It must be long enough for Codex `/responses/compact` requests; otherwise Caddy can return a client-visible 504 before Sub2API finishes the upstream compact request, and that edge timeout is not an account-level upstream failure that Sub2API can use for temporary-unschedulable failover. The numeric value belongs only in `config/platform-infra/sub2api-codex-pool.yaml`; after changing it, use `codex-pool expose --confirm` to reload Caddy and verify the rendered `response_header_timeout`. Requests that were already in flight before the reload may still finish with the previous timeout, so post-change evidence should check only requests that started after the reload. - `localCodex` controls how the master server's current `~/.codex` consumer files are backed up and rewritten. Keep `supportsWebSockets` and `responsesWebSocketsV2` in the same state, and enable them only when at least one YAML-managed account has a current direct Codex WSv2 smoke that passes. If no upstream profile can sustain Responses WSv2, the honest long-term state is `false/false` so Codex uses HTTP Responses directly instead of repeatedly reconnecting before `response.completed`. `localCodex.responsesSmokeModel` is the YAML-declared model used by `codex-pool validate` for the lightweight `POST /v1/responses` smoke. Enable account-level WebSocket v2 only for upstream profiles that have passed a direct Codex WSv2 probe. Treat this as a YAML-declared capability set, not a hard scheduling pin to one profile; if `localCodex` enables WebSocket transport, `codex-pool validate` must show at least one current `webSocketsV2.schedulableEnabled` account, and runtime smoke remains the availability proof. The same validation reports each managed account's runtime WebSocket v2 mode and whether it matches YAML, so stale `ctx_pool` / `passthrough` settings cannot silently keep routing Codex WS sessions to an upstream that closes with `no available account`, WS handshake 5xx/4xx, or before `response.completed`. diff --git a/scripts/agentrun-cli-contract-test.ts b/scripts/agentrun-cli-contract-test.ts index 663d58d6..6f360331 100644 --- a/scripts/agentrun-cli-contract-test.ts +++ b/scripts/agentrun-cli-contract-test.ts @@ -27,10 +27,11 @@ assertCondition( ); assertCondition( - agentRunUsage.some((line) => line.includes("queue submit --json-stdin --dry-run")) - && agentRunUsage.some((line) => line.includes("queue dispatch --json-stdin --dry-run")) - && agentRunUsage.some((line) => line.includes("queue commander --reader-id cli --limit 20")), - "AgentRun help must expose stdin-first queue dry-run and compact commander usage", + agentRunUsage.some((line) => line.includes("agentrun get tasks --queue commander --limit 20")) + && agentRunUsage.some((line) => line.includes("agentrun describe task/")) + && agentRunUsage.some((line) => line.includes("agentrun events run/ --after-seq 0 --limit 100")) + && agentRunUsage.some((line) => line.includes("agentrun logs session/ --tail 100")), + "AgentRun help must expose Kubernetes-style resource observation primitives", agentRunUsage, ); @@ -41,37 +42,27 @@ assertCondition( ); assertCondition( - agentRunUsage.some((line) => line.includes("queue show --full")) - && agentRunUsage.some((line) => line.includes("runs show ")) - && agentRunUsage.some((line) => line.includes("runs result --command-id ")) - && agentRunUsage.some((line) => line.includes("commands show --run-id ")) - && agentRunUsage.some((line) => line.includes("runner jobs --run-id --command-id ")) - && agentRunUsage.some((line) => line.includes("runner job-status --run-id ")) - && !agentRunUsage.some((line) => line.includes("queue lifecycle")), - "AgentRun help must expose queue progressive disclosure through existing show/run/command/runner commands without a lifecycle command", + agentRunUsage.some((line) => line.includes("agentrun result run/ --command ")) + && agentRunUsage.some((line) => line.includes("agentrun ack task/ --reader-id cli")) + && agentRunUsage.some((line) => line.includes("agentrun cancel task/ --reason --dry-run")) + && agentRunUsage.some((line) => line.includes("agentrun create task --aipod Artificer --prompt-stdin")) + && agentRunUsage.some((line) => line.includes("agentrun apply -f - --dry-run")) + && agentRunUsage.some((line) => line.includes("agentrun steer session/ --prompt-stdin")) + && agentRunUsage.some((line) => line.includes("agentrun send session/ --aipod Artificer --prompt-stdin")), + "AgentRun help must expose resource lifecycle control primitives", agentRunUsage, ); -const submitStdinIndex = agentRunUsage.findIndex((line) => line.includes("queue submit --json-stdin")); -const submitFileIndex = agentRunUsage.findIndex((line) => line.includes("queue submit --json-file")); -const dispatchStdinIndex = agentRunUsage.findIndex((line) => line.includes("queue dispatch --json-stdin")); -const dispatchFileIndex = agentRunUsage.findIndex((line) => line.includes("queue dispatch --json-file")); - assertCondition( - submitStdinIndex >= 0 - && dispatchStdinIndex >= 0 - && submitFileIndex > submitStdinIndex - && dispatchFileIndex > dispatchStdinIndex - && !agentRunUsage.some((line) => line.includes("queue submit --json-file --dry-run")) - && !agentRunUsage.some((line) => line.includes("queue dispatch --json-file --dry-run")), - "AgentRun help must present heredoc/stdin before reusable file fallbacks", - agentRunUsage, + (agentRunHelp() as { output?: unknown }).output === "human by default; use -o json|yaml or --raw for machine/debug output", + "AgentRun help must declare human output as the default and machine output as opt-in", + agentRunHelp(), ); const globalHelp = JSON.stringify(rootHelp()); assertCondition( - globalHelp.includes("agentrun aipod-specs|queue|runs|commands|runner|sessions|control-plane|git-mirror"), + globalHelp.includes("agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send"), "global help must index AgentRun v0.1 entrypoints", rootHelp(), ); @@ -105,10 +96,10 @@ console.log(JSON.stringify({ checks: [ "AgentRun command help exposes cleanup-runs and cleanup-released-pvs", "AgentRun command help exposes targeted control-plane status drill-down options", - "AgentRun command help exposes queue dry-run and compact commander usage", + "AgentRun command help exposes resource observation primitives", "AgentRun command help hides the v01 lane from user-facing CLI entrypoints", - "AgentRun command help exposes queue progressive disclosure without queue lifecycle", - "AgentRun command help presents heredoc/stdin before reusable file fallbacks", + "AgentRun command help exposes resource lifecycle control primitives", + "AgentRun command help declares human output by default", "global help indexes AgentRun v0.1 entrypoints", "AgentRun control-plane status degrades empty runtime JSON snippets", "AgentRun CLI bridge selects remote frontend backend in runner/no-Docker environments", diff --git a/scripts/cli.ts b/scripts/cli.ts index 18e4fe4c..dbfddd4a 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -1,7 +1,7 @@ import { readConfig } from "./src/config"; import { debugDispatch, debugHealth, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug"; import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService } from "./src/docker"; -import { emitError, emitJson } from "./src/output"; +import { emitError, emitJson, emitText, isRenderedCliResult } from "./src/output"; import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs"; import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check"; import { runSsh } from "./src/ssh"; @@ -232,6 +232,22 @@ async function main(): Promise { return; } + if (top === "agentrun") { + const { runAgentRunCommand } = await import("./src/agentrun"); + const agentRunArgs = args.slice(1); + const config = agentRunArgs.length === 0 || agentRunArgs.some(isHelpToken) ? null : readConfig(); + const result = await runAgentRunCommand(config, agentRunArgs); + const ok = (result as { ok?: unknown }).ok !== false; + if (isRenderedCliResult(result)) { + emitText(result.renderedText); + if (!ok) process.exitCode = 1; + return; + } + emitJson(commandName, result, ok); + if (!ok) process.exitCode = 1; + return; + } + const namespaceHelp = await staticNamespaceHelp(args); if (namespaceHelp !== null) { emitJson(commandName, namespaceHelp); @@ -313,15 +329,6 @@ async function main(): Promise { return; } - if (top === "agentrun") { - const { runAgentRunCommand } = await import("./src/agentrun"); - const result = await runAgentRunCommand(readConfig(), args.slice(1)); - const ok = (result as { ok?: unknown }).ok !== false; - emitJson(commandName, result, ok); - if (!ok) process.exitCode = 1; - return; - } - if (top === "platform-infra") { const { runPlatformInfraCommand } = await import("./src/platform-infra"); const result = await runPlatformInfraCommand(readConfig(), args.slice(1)); diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 8685473a..eff81e48 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import type { UniDeskConfig } from "./config"; +import type { RenderedCliResult } from "./output"; import { runSshCommandCapture, type SshCaptureResult } from "./ssh"; import { runRemoteSshCommandCapture } from "./remote"; import { startJob } from "./jobs"; @@ -25,43 +26,23 @@ const mirrorToolsImage = "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine export function agentRunHelp(): unknown { return { - command: "agentrun control-plane status|trigger-current|refresh|cleanup-runs|cleanup-released-pvs | git-mirror status|sync|flush | aipod-specs list|show|render|apply|delete | queue submit|list|show|stats|commander|read|cancel|dispatch|refresh | runs show|events|result | commands show|result | runner jobs|job-status | sessions ps|show|turn|steer|cancel|output|trace|read", - output: "json", + command: "agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send|explain", + output: "human by default; use -o json|yaml or --raw for machine/debug output", usage: [ - "bun scripts/cli.ts agentrun queue commander --reader-id cli", - "bun scripts/cli.ts agentrun queue commander --reader-id cli --limit 20", - "bun scripts/cli.ts agentrun queue commander --reader-id cli --full", - "bun scripts/cli.ts agentrun queue commander --reader-id cli --raw", - "bun scripts/cli.ts agentrun aipod-specs list", - "bun scripts/cli.ts agentrun aipod-specs show Artificer", - "bun scripts/cli.ts agentrun aipod-specs render Artificer --prompt-stdin", - "bun scripts/cli.ts agentrun aipod-specs apply --yaml-stdin --dry-run", - "bun scripts/cli.ts agentrun queue submit --json-stdin <<'JSON'", - "bun scripts/cli.ts agentrun queue submit --json-stdin --dry-run <<'JSON'", - "bun scripts/cli.ts agentrun queue submit --aipod Artificer --prompt-stdin --idempotency-key ", - "bun scripts/cli.ts agentrun queue submit --aipod Artificer --prompt-stdin --dry-run", - "bun scripts/cli.ts agentrun queue submit --json-file # reusable reviewed file fallback", - "bun scripts/cli.ts agentrun queue show ", - "bun scripts/cli.ts agentrun queue show --full", - "bun scripts/cli.ts agentrun queue show --raw", - "bun scripts/cli.ts agentrun queue dispatch --json-stdin <<'JSON'", - "bun scripts/cli.ts agentrun queue dispatch --json-stdin --dry-run <<'JSON'", - "bun scripts/cli.ts agentrun queue dispatch --json-file # reusable reviewed file fallback", - "bun scripts/cli.ts agentrun queue cancel --reason --dry-run", - "bun scripts/cli.ts agentrun runs show ", - "bun scripts/cli.ts agentrun runs show --full", - "bun scripts/cli.ts agentrun runs result --command-id ", - "bun scripts/cli.ts agentrun runs events --after-seq 0 --limit 100 --tail-summary", - "bun scripts/cli.ts agentrun commands show --run-id ", - "bun scripts/cli.ts agentrun commands show --run-id --full", - "bun scripts/cli.ts agentrun commands result --run-id ", - "bun scripts/cli.ts agentrun runner jobs --run-id --command-id ", - "bun scripts/cli.ts agentrun runner job-status --run-id ", - "bun scripts/cli.ts agentrun sessions trace --after-seq 0 --limit 100", - "bun scripts/cli.ts agentrun sessions output --after-seq 0 --limit 100", - "bun scripts/cli.ts agentrun sessions turn --aipod Artificer --prompt-stdin", - "bun scripts/cli.ts agentrun sessions steer --prompt-stdin", - "bun scripts/cli.ts agentrun sessions read --reader-id cli", + "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + "bun scripts/cli.ts agentrun get tasks -o wide", + "bun scripts/cli.ts agentrun get tasks -o json", + "bun scripts/cli.ts agentrun describe task/", + "bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", + "bun scripts/cli.ts agentrun logs session/ --tail 100", + "bun scripts/cli.ts agentrun result run/ --command ", + "bun scripts/cli.ts agentrun ack task/ --reader-id cli", + "bun scripts/cli.ts agentrun cancel task/ --reason --dry-run", + "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key ", + "bun scripts/cli.ts agentrun apply -f - --dry-run", + "bun scripts/cli.ts agentrun steer session/ --prompt-stdin", + "bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin", + "bun scripts/cli.ts agentrun explain task", "bun scripts/cli.ts agentrun control-plane status", "bun scripts/cli.ts agentrun control-plane status --full", "bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-", @@ -79,13 +60,19 @@ export function agentRunHelp(): unknown { "bun scripts/cli.ts agentrun git-mirror sync --confirm", "bun scripts/cli.ts agentrun git-mirror flush --confirm", ], - description: "Operate AgentRun v0.1 Queue, Sessions, Runs, Commands, Runner job status, and AipodSpec through the official G14 /root/agentrun-v01 CLI, plus bounded Tekton/Argo control-plane and devops-infra git mirror actions through UniDesk routes. Queue/session/aipod-spec/run/command/runner-read commands are direct AgentRun CLI calls, not a UniDesk Code Queue adapter or double-write path.", + resources: ["task/qt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"], + description: "Operate AgentRun v0.1 through Kubernetes-style resource verbs. Human output is compact by default; -o json|yaml returns the UniDesk resource schema, and --raw exposes the official G14 /root/agentrun-v01 CLI bridge response.", + legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain as compatibility bridge groups, but new commander work should use get/describe/events/logs/result/ack/cancel/create/apply/steer/send.", }; } -export async function runAgentRunCommand(config: UniDeskConfig, args: string[]): Promise> { +export async function runAgentRunCommand(config: UniDeskConfig | null, args: string[]): Promise | RenderedCliResult> { const route = normalizeAgentRunCommandArgs(args); const { group, action, actionArgs } = route; + if (group === undefined || isHelpArg(group)) return renderAgentRunHelp(route.canonicalArgs); + if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(route.canonicalArgs); + if (config === null) throw new Error("agentrun command requires config outside help"); + if (isResourceVerb(group)) return await runAgentRunResourceCommand(config, group, action, actionArgs, route.canonicalArgs); if (group === "control-plane") { if (action === "status") return await status(config, parseStatusOptions(actionArgs)); if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs)); @@ -134,6 +121,884 @@ function normalizeAgentRunCommandArgs(args: string[]): AgentRunCommandRoute { }; } +function isHelpArg(value: string | undefined): boolean { + return value === "help" || value === "--help" || value === "-h"; +} + +function isResourceVerb(value: string | undefined): value is AgentRunResourceVerb { + return value === "get" + || value === "describe" + || value === "events" + || value === "logs" + || value === "result" + || value === "ack" + || value === "cancel" + || value === "create" + || value === "apply" + || value === "steer" + || value === "send" + || value === "explain"; +} + +function renderAgentRunHelp(args: string[]): RenderedCliResult { + const text = agentRunHelpText(args); + return renderedCliResult(true, "agentrun help", text); +} + +function agentRunHelpText(args: string[]): string { + const [verb, kind] = args.filter((arg) => !isHelpArg(arg)); + if (verb === "get" && kind !== undefined) return agentRunGetKindHelp(kind); + if (verb === "get") { + return [ + "Usage: bun scripts/cli.ts agentrun get [options]", + "", + "Resources: tasks|sessions|runs|commands|runnerjobs|aipodspecs", + "Output: table by default; use -o wide|name|json|yaml.", + "", + "Examples:", + " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + " bun scripts/cli.ts agentrun get sessions --limit 20", + " bun scripts/cli.ts agentrun get tasks -o json", + ].join("\n"); + } + if (verb === "describe") { + return [ + "Usage: bun scripts/cli.ts agentrun describe [--full] [-o json|yaml] [--raw]", + "", + "Kinds: task|run|command|runnerjob|session|aipodspec", + "Examples:", + " bun scripts/cli.ts agentrun describe task/qt_...", + " bun scripts/cli.ts agentrun describe command/cmd_... --run ", + " bun scripts/cli.ts agentrun describe session/ --full", + ].join("\n"); + } + if (verb === "events") { + return "Usage: bun scripts/cli.ts agentrun events run/ [--after-seq N] [--limit 100] [-o json|yaml] [--raw]"; + } + if (verb === "logs") { + return "Usage: bun scripts/cli.ts agentrun logs session/ [--tail 100|--after-seq N] [--limit 100] [--full-text] [-o json|yaml] [--raw]"; + } + if (verb === "result") { + return "Usage: bun scripts/cli.ts agentrun result run/ --command | agentrun result command/ --run "; + } + if (verb === "ack") { + return "Usage: bun scripts/cli.ts agentrun ack task/|session/ [--reader-id cli]"; + } + if (verb === "cancel") { + return "Usage: bun scripts/cli.ts agentrun cancel task/|session/ --reason [--dry-run]"; + } + if (verb === "create") { + return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin [--idempotency-key ] [--dry-run]"; + } + if (verb === "apply") { + return "Usage: bun scripts/cli.ts agentrun apply -f task.yaml|json|- [--dry-run]\nTask manifests use kind: Task and spec: ."; + } + if (verb === "steer") { + return "Usage: bun scripts/cli.ts agentrun steer session/ --prompt-stdin"; + } + if (verb === "send") { + return "Usage: bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin"; + } + if (verb === "explain") return agentRunExplain(kind ?? "task"); + if (verb === "control-plane") { + return [ + "Usage: bun scripts/cli.ts agentrun control-plane [options]", + "", + "Actions: status, trigger-current, refresh, cleanup-runs, cleanup-released-pvs", + "Examples:", + " bun scripts/cli.ts agentrun control-plane status", + " bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-", + " bun scripts/cli.ts agentrun control-plane trigger-current --dry-run", + " bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run", + ].join("\n"); + } + if (verb === "git-mirror") { + return [ + "Usage: bun scripts/cli.ts agentrun git-mirror [--full|--raw|--confirm]", + "", + "Confirmed sync/flush returns an async job unless --wait is set.", + ].join("\n"); + } + if (verb !== undefined && isOfficialAgentRunCliBridgeGroup(verb)) { + return [ + `Compatibility bridge: agentrun ${verb} ...`, + "", + "Use resource primitives for daily commander work:", + " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + " bun scripts/cli.ts agentrun describe task/", + " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", + " bun scripts/cli.ts agentrun logs session/ --tail 100", + "", + "Use --raw on a resource command when you need the official bridge response.", + ].join("\n"); + } + return [ + "Usage: bun scripts/cli.ts agentrun [options]", + "", + "Verbs: get, describe, events, logs, result, ack, cancel, create, apply, steer, send, explain", + "Resources: task/qt, run, command/cmd, runnerjob/rjob, session/ses, aipodspec/aps", + "", + "Common:", + " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + " bun scripts/cli.ts agentrun describe task/", + " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", + " bun scripts/cli.ts agentrun logs session/ --tail 100", + " bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", + "", + "Machine/debug output:", + " -o json|yaml emits a stable UniDesk resource object without the global JSON envelope.", + " --raw emits the official AgentRun CLI bridge response.", + ].join("\n"); +} + +function agentRunGetKindHelp(kindRaw: string): string { + const kind = parseResourceKind(kindRaw); + if (kind === "task") return "Usage: bun scripts/cli.ts agentrun get tasks [--queue commander] [--state running,completed,failed] [--unread] [--limit 20] [-o wide|name|json|yaml]"; + if (kind === "session") return "Usage: bun scripts/cli.ts agentrun get sessions [--limit 20] [-o wide|name|json|yaml]"; + if (kind === "run") return "Usage: bun scripts/cli.ts agentrun get runs --task [--limit 20] [-o wide|name|json|yaml]"; + if (kind === "command") return "Usage: bun scripts/cli.ts agentrun get commands --run [--command ] [-o wide|name|json|yaml]"; + if (kind === "runnerjob") return "Usage: bun scripts/cli.ts agentrun get runnerjobs --run --command [-o wide|name|json|yaml]"; + if (kind === "aipodspec") return "Usage: bun scripts/cli.ts agentrun get aipodspecs [-o wide|name|json|yaml]"; + return "Unknown resource. Supported: tasks, sessions, runs, commands, runnerjobs, aipodspecs."; +} + +async function runAgentRunResourceCommand(config: UniDeskConfig, verb: AgentRunResourceVerb, action: string | undefined, actionArgs: string[], canonicalArgs: string[]): Promise { + if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(canonicalArgs); + if (verb === "explain") return renderedCliResult(true, "agentrun explain", agentRunExplain(action ?? "task")); + const resourceArgs = action === undefined ? actionArgs : [action, ...actionArgs]; + const options = parseResourceOptions(resourceArgs); + const command = `agentrun ${canonicalArgs.join(" ")}`.trim(); + try { + if (verb === "get") return await resourceGet(config, command, action, actionArgs, options); + if (verb === "describe") return await resourceDescribe(config, command, action, actionArgs, options); + if (verb === "events") return await resourceEvents(config, command, action, actionArgs, options); + if (verb === "logs") return await resourceLogs(config, command, action, actionArgs, options); + if (verb === "result") return await resourceResult(config, command, action, actionArgs, options); + if (verb === "ack") return await resourceAck(config, command, action, actionArgs, options); + if (verb === "cancel") return await resourceCancel(config, command, action, actionArgs, options); + if (verb === "create") return await resourceCreate(config, command, action, actionArgs, options); + if (verb === "apply") return await resourceApply(config, command, actionArgs, options); + if (verb === "steer") return await resourceSessionPromptCommand(config, command, "steer", action, actionArgs, options); + if (verb === "send") return await resourceSessionPromptCommand(config, command, "turn", action, actionArgs, options); + } catch (error) { + return renderedCliResult(false, command, `Error: ${error instanceof Error ? error.message : String(error)}`); + } + return renderedCliResult(false, command, `Unsupported AgentRun resource command. Try: bun scripts/cli.ts agentrun --help`); +} + +function parseResourceOptions(args: string[]): AgentRunResourceOptions { + const options: AgentRunResourceOptions = { + output: "human", + full: false, + raw: false, + debug: false, + limit: 20, + queue: null, + state: null, + unread: false, + readerId: "cli", + taskId: null, + runId: null, + commandId: null, + sessionId: null, + afterSeq: null, + tail: null, + fullText: false, + reason: null, + dryRun: false, + file: null, + aipod: null, + idempotencyKey: null, + promptStdin: false, + passthroughArgs: [], + }; + const valueFlags = new Set(["-o", "--output", "--limit", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key"]); + const booleanFlags = new Set(["--full", "--raw", "--debug", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]); + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] ?? ""; + if (!arg.startsWith("-")) continue; + if (arg.startsWith("--") && arg.includes("=")) { + const [flag, ...rest] = arg.split("="); + applyResourceOption(options, flag, rest.join("=")); + continue; + } + if (booleanFlags.has(arg)) { + applyResourceOption(options, arg, null); + continue; + } + if (valueFlags.has(arg)) { + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); + applyResourceOption(options, arg, value); + index += 1; + continue; + } + } + return options; +} + +function applyResourceOption(options: AgentRunResourceOptions, flag: string, value: string | null): void { + if (flag === "-o" || flag === "--output") { + if (value !== "human" && value !== "wide" && value !== "name" && value !== "json" && value !== "yaml") throw new Error(`${flag} must be one of human,wide,name,json,yaml`); + options.output = value; + return; + } + if (flag === "--full") options.full = true; + else if (flag === "--raw") options.raw = true; + else if (flag === "--debug") options.debug = true; + else if (flag === "--unread") options.unread = true; + else if (flag === "--dry-run") options.dryRun = true; + else if (flag === "--full-text") options.fullText = true; + else if (flag === "--prompt-stdin" || flag === "--stdin") options.promptStdin = true; + else if (flag === "--limit") options.limit = parseNonNegativeInt(value, "--limit", 20, 500); + else if (flag === "--queue") options.queue = requiredValue(value, flag); + else if (flag === "--state") options.state = requiredValue(value, flag); + else if (flag === "--reader-id") options.readerId = requiredValue(value, flag); + else if (flag === "--task" || flag === "--task-id") options.taskId = requiredValue(value, flag); + else if (flag === "--run" || flag === "--run-id") options.runId = requiredValue(value, flag); + else if (flag === "--command" || flag === "--command-id") options.commandId = requiredValue(value, flag); + else if (flag === "--session" || flag === "--session-id") options.sessionId = requiredValue(value, flag); + else if (flag === "--after-seq") options.afterSeq = parseNonNegativeInt(value, "--after-seq", 0, Number.MAX_SAFE_INTEGER); + else if (flag === "--tail") options.tail = parseNonNegativeInt(value, "--tail", 100, 1000); + else if (flag === "--reason") options.reason = requiredValue(value, flag); + else if (flag === "-f" || flag === "--file" || flag === "--filename") options.file = requiredValue(value, flag); + else if (flag === "--aipod") options.aipod = requiredValue(value, flag); + else if (flag === "--idempotency-key") options.idempotencyKey = requiredValue(value, flag); +} + +function parseNonNegativeInt(raw: string | null, flag: string, defaultValue: number, maxValue: number): number { + if (raw === null || raw.length === 0) return defaultValue; + const value = Number(raw); + if (!Number.isInteger(value) || value < 0) throw new Error(`${flag} must be a non-negative integer`); + return Math.min(value, maxValue); +} + +function requiredValue(value: string | null, flag: string): string { + if (value === null || value.length === 0) throw new Error(`${flag} requires a value`); + return value; +} + +async function resourceGet(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const kind = parseResourceKind(action); + if (kind === null) throw new Error("get requires a resource: tasks|sessions|runs|commands|runnerjobs|aipodspecs"); + let result: Record; + if (kind === "task") { + result = await runOfficialAgentRunCli(config, "queue", ["commander", "--reader-id", options.readerId, "--limit", String(options.limit)]); + return renderResourceResult(command, result, options, "Task", normalizeTaskItems(innerData(result), options).slice(0, options.limit)); + } + if (kind === "session") { + result = await runOfficialAgentRunCli(config, "sessions", ["ps", "--limit", String(options.limit)]); + return renderResourceResult(command, result, options, "Session", normalizeSessionItems(innerData(result)).slice(0, options.limit)); + } + if (kind === "aipodspec") { + result = await runOfficialAgentRunCli(config, "aipod-specs", ["list"]); + return renderResourceResult(command, result, options, "AipodSpec", normalizeAipodSpecItems(innerData(result)).slice(0, options.limit)); + } + if (kind === "run" && options.taskId !== null) { + result = await runOfficialAgentRunCli(config, "queue", ["show", options.taskId]); + const task = record(innerData(result)); + return renderResourceResult(command, result, options, "Run", normalizeAttemptResources(task, "run")); + } + if (kind === "command" && options.runId !== null && options.commandId !== null) { + result = await runOfficialAgentRunCli(config, "commands", ["show", options.commandId, "--run-id", options.runId]); + return renderResourceResult(command, result, options, "Command", normalizeSingleCommand(innerData(result))); + } + if (kind === "runnerjob" && options.runId !== null && options.commandId !== null) { + result = await runOfficialAgentRunCli(config, "runner", ["jobs", "--run-id", options.runId, "--command-id", options.commandId]); + return renderResourceResult(command, result, options, "RunnerJob", normalizeRunnerJobItems(innerData(result))); + } + throw new Error(`get ${kind}s requires more context; use --task, --run/--command, or describe /`); +} + +async function resourceDescribe(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args); + if (ref.kind === "task") { + const result = await runOfficialAgentRunCli(config, "queue", ["show", ref.name, ...(options.full ? ["--full"] : [])]); + const data = record(innerData(result)); + const task = unwrapTaskDetail(data); + if (options.raw) return renderMachine(command, result, "json", result.ok !== false); + if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: ref.kind, name: ref.name, resource: task }, options.output, result.ok !== false); + return renderedCliResult(result.ok !== false, command, renderTaskDescription(task, options)); + } + if (ref.kind === "run") { + const result = await runOfficialAgentRunCli(config, "runs", ["show", ref.name, ...(options.full ? ["--full"] : [])]); + return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + } + if (ref.kind === "command") { + const runId = options.runId ?? requiredContext("command describe", "--run "); + const result = await runOfficialAgentRunCli(config, "commands", ["show", ref.name, "--run-id", runId, ...(options.full ? ["--full"] : [])]); + return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + } + if (ref.kind === "runnerjob") { + const runId = options.runId ?? requiredContext("runnerjob describe", "--run "); + const result = await runOfficialAgentRunCli(config, "runner", ["job-status", ref.name, "--run-id", runId]); + return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + } + if (ref.kind === "session") { + const result = await runOfficialAgentRunCli(config, "sessions", ["show", ref.name, ...(options.full ? ["--full"] : [])]); + return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + } + if (ref.kind === "aipodspec") { + const result = await runOfficialAgentRunCli(config, "aipod-specs", ["show", ref.name, ...(options.full ? ["--full"] : [])]); + return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + } + throw new Error(`unsupported describe kind: ${ref.kind}`); +} + +async function resourceEvents(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args, "run"); + const runId = ref.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run "); + const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(options.limit), "--tail-summary"]; + const result = await runOfficialAgentRunCli(config, "runs", eventArgs); + return renderEventLike(command, result, options, "Event", normalizeEventItems(innerData(result)), runId); +} + +async function resourceLogs(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args, "session"); + if (ref.kind !== "session") throw new Error("logs currently requires session/"); + const effectiveLimit = options.tail ?? options.limit; + const logArgs = ["output", ref.name, "--limit", String(effectiveLimit)]; + if (options.afterSeq !== null) logArgs.push("--after-seq", String(options.afterSeq)); + else logArgs.push("--tail"); + if (options.fullText) logArgs.push("--full-text"); + const result = await runOfficialAgentRunCli(config, "sessions", logArgs); + return renderEventLike(command, result, { ...options, limit: effectiveLimit }, "Log", normalizeLogItems(innerData(result)), ref.name); +} + +async function resourceResult(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args); + if (ref.kind === "run") { + const commandId = options.commandId ?? requiredContext("run result", "--command "); + const result = await runOfficialAgentRunCli(config, "runs", ["result", ref.name, "--command-id", commandId]); + return renderResultSummary(command, result, options, ref); + } + if (ref.kind === "command") { + const runId = options.runId ?? requiredContext("command result", "--run "); + const result = await runOfficialAgentRunCli(config, "commands", ["result", ref.name, "--run-id", runId]); + return renderResultSummary(command, result, options, ref); + } + throw new Error("result supports run/ or command/"); +} + +async function resourceAck(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args, "task"); + const result = ref.kind === "task" + ? await runOfficialAgentRunCli(config, "queue", ["read", ref.name, "--reader-id", options.readerId]) + : ref.kind === "session" + ? await runOfficialAgentRunCli(config, "sessions", ["read", ref.name, "--reader-id", options.readerId]) + : null; + if (result === null) throw new Error("ack supports task/ or session/"); + return renderMutationSummary(command, result, options, `Acknowledged ${ref.kind}/${shortId(ref.name)}`); +} + +async function resourceCancel(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args, "task"); + const cancelArgs = ref.kind === "task" ? ["cancel", ref.name] : ref.kind === "session" ? ["cancel", ref.name] : null; + if (cancelArgs === null) throw new Error("cancel supports task/ or session/"); + if (options.reason !== null && ref.kind === "task") cancelArgs.push("--reason", options.reason); + if (options.dryRun) cancelArgs.push("--dry-run"); + const result = ref.kind === "task" + ? await runOfficialAgentRunCli(config, "queue", cancelArgs) + : await runOfficialAgentRunCli(config, "sessions", cancelArgs); + return renderMutationSummary(command, result, options, `${options.dryRun ? "Planned cancel" : "Cancel requested"} ${ref.kind}/${shortId(ref.name)}`, options.dryRun ? [rerunWithoutDryRun(command)] : undefined); +} + +async function resourceCreate(config: UniDeskConfig, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const kind = parseResourceKind(action); + if (kind !== "task") throw new Error("create currently supports: create task"); + const submitArgs = ["submit", ...stripLeadingResource(args, "task")]; + const result = await runOfficialAgentRunCli(config, "queue", submitArgs); + return renderMutationSummary(command, result, options, "Task create submitted", options.dryRun ? [rerunWithoutDryRun(command)] : undefined); +} + +async function resourceApply(config: UniDeskConfig, command: string, args: string[], options: AgentRunResourceOptions): Promise { + const file = options.file ?? requiredContext("apply", "-f |-"); + const raw = file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8"); + const parsed = parseTaskManifest(raw, file); + const json = `${JSON.stringify(parsed, null, 2)}\n`; + const submitArgs = ["queue", "submit", "--json-stdin", ...(options.dryRun ? ["--dry-run"] : [])]; + const prepared: PreparedAgentRunCliArgs = { + args: submitArgs, + materializedFiles: [], + stdinPayload: { + flag: "--json-stdin", + source: file, + bytes: Buffer.byteLength(json, "utf8"), + base64: Buffer.from(json, "utf8").toString("base64"), + }, + }; + const result = await runPreparedOfficialAgentRunCli(config, prepared); + return renderMutationSummary(command, result, options, `${options.dryRun ? "Dry-run applied" : "Applied"} task manifest`, options.dryRun ? [rerunWithoutDryRun(command)] : undefined); +} + +async function resourceSessionPromptCommand(config: UniDeskConfig, command: string, officialAction: "steer" | "turn", action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + const ref = parseResourceRef(action, args, "session"); + if (ref.kind !== "session") throw new Error(`${officialAction === "steer" ? "steer" : "send"} requires session/`); + const sessionArgs = [officialAction, ref.name, ...stripLeadingResource(args, ref.name)]; + const result = await runOfficialAgentRunCli(config, "sessions", sessionArgs); + return renderMutationSummary(command, result, options, officialAction === "steer" ? "Steer submitted" : "Session turn submitted"); +} + +function renderedCliResult(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult { + return { ok, command, renderedText, contentType }; +} + +function renderResourceResult(command: string, raw: Record, options: AgentRunResourceOptions, kindLabel: string, items: Record[]): RenderedCliResult { + if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); + const payload = { kind: `${kindLabel}List`, count: items.length, items }; + if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output); + if (options.output === "name") return renderedCliResult(raw.ok !== false, command, items.map((item) => `${String(item.kind ?? kindLabel).toLowerCase()}/${String(item.name ?? "")}`).join("\n")); + if (items.length === 0) return renderedCliResult(raw.ok !== false, command, `No ${kindLabel.toLowerCase()} resources found.`); + return renderedCliResult(raw.ok !== false, command, renderResourceTable(items, options.output === "wide")); +} + +function renderDescribe(command: string, raw: Record, options: AgentRunResourceOptions, ref: AgentRunResourceRef, text: string): RenderedCliResult { + if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); + if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: ref.kind, name: ref.name, resource: innerData(raw) }, options.output, raw.ok !== false); + return renderedCliResult(raw.ok !== false, command, text); +} + +function renderEventLike(command: string, raw: Record, options: AgentRunResourceOptions, kind: string, items: Record[], sourceName: string): RenderedCliResult { + if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); + const data = record(innerData(raw)); + const payload = { kind: `${kind}List`, source: sourceName, nextAfterSeq: data.nextAfterSeq ?? null, count: items.length, items }; + if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false); + const lines = [ + `${kind}s for ${sourceName}`, + renderTable(["SEQ", "TYPE", "STATUS", "COMMAND", "SUMMARY"], items.map((item) => [ + String(item.seq ?? "-"), + String(item.type ?? "-"), + String(item.status ?? item.phase ?? "-"), + shortId(String(item.commandId ?? "-")), + truncateOneLine(String(item.summary ?? item.text ?? "") || "-", 96), + ])), + ]; + if (data.nextAfterSeq !== undefined && data.nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(data.nextAfterSeq), options.limit)}`); + return renderedCliResult(raw.ok !== false, command, lines.join("\n")); +} + +function renderResultSummary(command: string, raw: Record, options: AgentRunResourceOptions, ref: AgentRunResourceRef): RenderedCliResult { + if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); + const data = record(innerData(raw)); + if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: "Result", ref, result: data }, options.output, raw.ok !== false); + const lines = [ + `Result: ${ref.kind}/${shortId(ref.name)}`, + `State: ${displayValue(data.state ?? data.status ?? data.terminalStatus ?? "-")}`, + `OK: ${String(raw.ok !== false)}`, + ]; + const final = stringOrNull(data.finalResponse) ?? stringOrNull(data.output) ?? stringOrNull(data.summary) ?? stringOrNull(data.result); + if (final !== null) lines.push("", truncateMultiline(final, options.fullText ? 8000 : 1600)); + else lines.push("", JSON.stringify(pickCompact(data, ["failureKind", "terminalClassification", "degradedReason", "message", "valuesPrinted"]), null, 2)); + return renderedCliResult(raw.ok !== false, command, lines.join("\n")); +} + +function renderMutationSummary(command: string, raw: Record, options: AgentRunResourceOptions, headline: string): RenderedCliResult { + if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); + if (options.output === "json" || options.output === "yaml") return renderMachine(command, raw, options.output, raw.ok !== false); + const data = record(innerData(raw)); + const id = stringOrNull(data.id) ?? stringOrNull(data.taskId) ?? stringOrNull(data.sessionId); + const lines = [ + headline, + `OK: ${String(raw.ok !== false)}`, + ]; + if (id !== null) lines.push(`Name: ${id}`); + const next = record(raw.next ?? data.next); + const nextLines = Object.values(next).map(String).filter((line) => line.length > 0).slice(0, 5); + if (nextLines.length > 0) lines.push("", "Next:", ...nextLines.map((line) => ` ${line}`)); + return renderedCliResult(raw.ok !== false, command, lines.join("\n")); +} + +function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult { + const text = mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`; + return renderedCliResult(ok, command, text, mode === "json" ? "application/json" : "application/yaml"); +} + +function parseResourceKind(raw: string | undefined): AgentRunResourceKind | null { + if (raw === undefined) return null; + const value = raw.toLowerCase().replace(/s$/u, ""); + if (value === "task" || value === "qt" || value === "queue" || value === "queuetask") return "task"; + if (value === "run") return "run"; + if (value === "command" || value === "cmd") return "command"; + if (value === "runnerjob" || value === "runner-job" || value === "rjob" || value === "job") return "runnerjob"; + if (value === "session" || value === "ses") return "session"; + if (value === "aipodspec" || value === "aipod-spec" || value === "aipod-specs" || value === "aps") return "aipodspec"; + return null; +} + +function parseResourceRef(action: string | undefined, args: string[], defaultKind: AgentRunResourceKind | null = null): AgentRunResourceRef { + const raw = action ?? ""; + if (raw.includes("/")) { + const [kindRaw, ...nameParts] = raw.split("/"); + const kind = parseResourceKind(kindRaw); + const name = nameParts.join("/"); + if (kind === null || name.length === 0) throw new Error(`invalid resource reference: ${raw}`); + return { kind, name }; + } + const kind = parseResourceKind(raw); + if (kind !== null) { + const name = args.find((arg) => !arg.startsWith("-") && arg !== raw); + if (name === undefined) throw new Error(`${kind} reference requires a name`); + return { kind, name }; + } + if (defaultKind !== null && raw.length > 0) return { kind: defaultKind, name: raw }; + throw new Error("resource reference required, for example task/ or session/"); +} + +function stripLeadingResource(args: string[], resourceName: string): string[] { + let skipped = false; + const result: string[] = []; + for (const arg of args) { + if (!skipped && (arg === resourceName || arg.endsWith(`/${resourceName}`) || parseResourceKind(arg) !== null)) { + skipped = true; + continue; + } + result.push(arg); + } + return result; +} + +function requiredContext(context: string, flag: string): never { + throw new Error(`${context} requires ${flag}`); +} + +function innerData(raw: Record): unknown { + const data = raw.data; + if (typeof data === "object" && data !== null && "data" in data && Object.keys(raw).length <= 3) return record(data).data; + return data ?? raw; +} + +function normalizeTaskItems(data: unknown, options: AgentRunResourceOptions): Record[] { + const items = arrayRecords(record(data).items ?? data); + return items + .filter((item) => options.queue === null || String(item.queue ?? "") === options.queue) + .filter((item) => options.state === null || options.state.split(",").includes(String(item.state ?? ""))) + .filter((item) => !options.unread || item.unread === true || item.attention === true) + .map((item) => { + const attempt = record(item.latestAttempt); + return { + kind: "task", + name: item.id, + state: item.state, + queue: item.queue, + lane: item.lane, + profile: item.backendProfile, + run: attempt.runId, + command: attempt.commandId, + runnerjob: attempt.runnerJobId, + session: attempt.sessionId ?? stringOrNull(record(item.sessionRef).sessionId), + age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)), + title: item.title, + updatedAt: item.updatedAt, + attention: item.unread === true || item.attention === true ? "unread" : "", + }; + }); +} + +function normalizeSessionItems(data: unknown): Record[] { + const items = arrayRecords(record(data).items ?? data); + return items.map((item) => ({ + kind: "session", + name: item.id ?? item.sessionId ?? item.name, + state: item.state ?? item.status ?? item.phase ?? "-", + queue: item.queue ?? "", + lane: item.lane ?? "", + profile: item.backendProfile ?? item.profile ?? "", + run: item.runId ?? "", + command: item.commandId ?? "", + runnerjob: item.runnerJobId ?? "", + session: item.id ?? item.sessionId ?? item.name, + age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)), + title: item.title ?? "", + })); +} + +function normalizeAipodSpecItems(data: unknown): Record[] { + const items = arrayRecords(record(data).items ?? data); + return items.map((item) => ({ + kind: "aipodspec", + name: item.name ?? item.id ?? item.metadata?.toString(), + state: item.state ?? "available", + queue: "", + lane: item.lane ?? "", + profile: item.backendProfile ?? item.profile ?? "", + title: item.description ?? item.title ?? "", + })); +} + +function normalizeAttemptResources(task: Record, kind: "run" | "command" | "runnerjob"): Record[] { + const attempt = record(task.latestAttempt); + const key = kind === "run" ? "runId" : kind === "command" ? "commandId" : "runnerJobId"; + const name = stringOrNull(attempt[key]); + if (name === null) return []; + return [{ + kind, + name, + state: attempt.state ?? task.state, + queue: task.queue, + lane: task.lane, + run: attempt.runId, + command: attempt.commandId, + runnerjob: attempt.runnerJobId, + session: attempt.sessionId, + title: task.title, + }]; +} + +function normalizeSingleCommand(data: unknown): Record[] { + const item = record(data); + return [{ + kind: "command", + name: item.id ?? item.commandId ?? "", + state: item.state ?? item.status ?? "", + run: item.runId ?? "", + command: item.id ?? item.commandId ?? "", + session: item.sessionId ?? "", + title: item.summary ?? item.commandType ?? "", + }]; +} + +function normalizeRunnerJobItems(data: unknown): Record[] { + const items = arrayRecords(record(data).items ?? data); + return items.map((item) => ({ + kind: "runnerjob", + name: item.id ?? item.runnerJobId ?? item.name ?? item.jobName, + state: item.phase ?? item.state ?? item.status ?? "", + run: item.runId ?? "", + command: item.commandId ?? "", + runnerjob: item.id ?? item.runnerJobId ?? item.name ?? item.jobName, + session: item.sessionId ?? "", + title: item.image ?? item.namespace ?? "", + })); +} + +function normalizeEventItems(data: unknown): Record[] { + return arrayRecords(record(data).items ?? data).map((item) => ({ + seq: item.seq, + type: item.type, + status: item.status ?? item.phase, + phase: item.phase, + commandId: item.commandId, + summary: item.summary ?? item.outputSummary ?? item.text ?? "", + })); +} + +function normalizeLogItems(data: unknown): Record[] { + return arrayRecords(record(data).items ?? data).map((item) => ({ + seq: item.seq, + type: item.type ?? item.role ?? "output", + status: item.status ?? item.phase ?? "", + commandId: item.commandId ?? "", + summary: item.summary ?? item.text ?? item.content ?? item.output ?? "", + })); +} + +function renderResourceTable(items: Record[], wide: boolean): string { + const headers = wide + ? ["NAME", "STATE", "QUEUE", "LANE", "PROFILE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "TITLE"] + : ["NAME", "STATE", "QUEUE", "LANE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "ATTENTION"]; + const rows = items.map((item) => { + if (wide) { + return [ + resourceName(item), + displayValue(item.state), + displayValue(item.queue), + displayValue(item.lane), + displayValue(item.profile), + shortId(stringOrDash(item.run)), + shortId(stringOrDash(item.command)), + shortId(stringOrDash(item.runnerjob)), + shortId(stringOrDash(item.session)), + displayValue(item.age), + truncateOneLine(displayValue(item.title), 48), + ]; + } + return [ + resourceName(item), + displayValue(item.state), + displayValue(item.queue), + displayValue(item.lane), + shortId(stringOrDash(item.run)), + shortId(stringOrDash(item.command)), + shortId(stringOrDash(item.runnerjob)), + shortId(stringOrDash(item.session)), + displayValue(item.age), + displayValue(item.attention), + ]; + }); + return renderTable(headers, rows); +} + +function renderTaskDescription(task: Record, options: AgentRunResourceOptions): string { + const attempt = record(task.latestAttempt); + const taskId = displayValue(task.id); + const runId = stringOrNull(attempt.runId); + const commandId = stringOrNull(attempt.commandId); + const runnerJobId = stringOrNull(attempt.runnerJobId); + const sessionId = stringOrNull(attempt.sessionId) ?? stringOrNull(record(task.sessionRef).sessionId); + const lines = [ + `Name: task/${taskId}`, + `State: ${displayValue(task.state)} Attention: ${displayValue(task.unread === true ? "unread" : "")}`, + `Queue: ${displayValue(task.queue)}`, + `Lane: ${displayValue(task.lane)} Profile: ${displayValue(task.backendProfile)} Provider: ${displayValue(task.providerId)}`, + `Title: ${displayValue(task.title)}`, + "", + "Latest Attempt:", + ` Attempt: ${displayValue(attempt.attemptId)} State: ${displayValue(attempt.state)}`, + ` Run: ${runId === null ? "-" : `run/${runId}`}`, + ` Command: ${commandId === null ? "-" : `command/${commandId}`}`, + ` Runner: ${runnerJobId === null ? "-" : `runnerjob/${runnerJobId}`}`, + ` Session: ${sessionId === null ? "-" : `session/${sessionId}`}`, + "", + "Conditions:", + ` Terminal: ${displayValue(task.state)}`, + ` Failure: ${displayValue(task.failureKind ?? task.degradedReason ?? "none")}`, + " Values: redacted", + "", + "Next:", + ]; + const next = [ + runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`, + sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`, + runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`, + `bun scripts/cli.ts agentrun ack task/${taskId}`, + options.full ? null : `bun scripts/cli.ts agentrun describe task/${taskId} --full`, + ].filter((item): item is string => item !== null).slice(0, 5); + lines.push(...next.map((item) => ` ${item}`)); + return lines.join("\n"); +} + +function unwrapTaskDetail(data: Record): Record { + const task = record(data.task); + if (Object.keys(task).length > 0) return task; + return data; +} + +function renderGenericDescription(ref: AgentRunResourceRef, data: unknown): string { + const value = record(data); + const lines = [ + `Name: ${ref.kind}/${ref.name}`, + `State: ${displayValue(value.state ?? value.status ?? value.phase ?? "-")}`, + "", + "Summary:", + JSON.stringify(pickCompact(value, ["id", "name", "runId", "commandId", "runnerJobId", "sessionId", "state", "status", "phase", "reason", "failureKind", "terminalClassification", "valuesPrinted"]), null, 2), + ]; + return lines.join("\n"); +} + +function parseTaskManifest(raw: string, source: string): Record { + const parsed = source.endsWith(".json") ? JSON.parse(raw) as unknown : Bun.YAML.parse(raw) as unknown; + const input = record(parsed); + const kind = stringOrNull(input.kind); + if (kind !== null && kind.toLowerCase() !== "task") throw new Error("apply currently supports kind: Task only"); + const spec = record(input.spec); + const payload = Object.keys(spec).length > 0 ? spec : input; + if (!isRecord(payload.payload) && !isRecord(payload.executionPolicy) && stringOrNull(payload.title) === null) { + throw new Error("task manifest must contain spec with an AgentRun queue submit payload"); + } + return payload; +} + +function nextPagedResourceCommand(command: string, nextAfterSeq: string, limit: number): string { + const parts = command + .replace(/\s+--after-seq\s+\S+/gu, "") + .replace(/\s+--tail(?:\s+\S+)?/gu, "") + .replace(/\s+--limit\s+\S+/gu, "") + .trim(); + return `${parts} --after-seq ${nextAfterSeq} --limit ${limit}`; +} + +function agentRunExplain(kindRaw: string): string { + const kind = parseResourceKind(kindRaw); + if (kind === "task") { + return [ + "KIND: task", + "FIELDS:", + " metadata.name: optional client name/idempotency hint", + " spec.tenantId, spec.projectId, spec.queue, spec.title", + " spec.payload.prompt or spec.payload.", + " spec.executionPolicy, spec.resourceBundleRef, spec.workspaceRef", + "", + "Apply example:", + " kind: Task", + " spec:", + " tenantId: unidesk", + " projectId: pikasTech/unidesk", + " queue: commander", + " title: Example", + " payload:", + " prompt: Do the work", + ].join("\n"); + } + return `KIND: ${kindRaw}\nUse get/describe with -o json to inspect the current stable UniDesk resource schema.`; +} + +function arrayRecords(value: unknown): Record[] { + if (Array.isArray(value)) return value.map(record).filter((item) => Object.keys(item).length > 0); + if (typeof value === "object" && value !== null && Array.isArray((value as { items?: unknown }).items)) return arrayRecords((value as { items: unknown }).items); + return []; +} + +function renderTable(headers: string[], rows: string[][]): string { + const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))); + const line = (values: string[]): string => values.map((value, index) => value.padEnd(widths[index] ?? value.length)).join(" ").trimEnd(); + return [line(headers), ...rows.map(line)].join("\n"); +} + +function resourceName(item: Record): string { + const kind = String(item.kind ?? "resource"); + return `${kind}/${shortId(displayValue(item.name))}`; +} + +function shortId(value: string): string { + if (value === "-" || value.length <= 18) return value; + const known = ["qt_", "run_", "cmd_", "rjob_"]; + const prefix = known.find((item) => value.startsWith(item)); + if (prefix !== undefined) return `${prefix}${value.slice(prefix.length, prefix.length + 8)}...`; + if (/^\w+-\d+/u.test(value)) return value.length > 28 ? `${value.slice(0, 25)}...` : value; + return `${value.slice(0, 14)}...`; +} + +function displayValue(value: unknown): string { + if (value === undefined || value === null || value === "") return "-"; + return String(value); +} + +function stringOrDash(value: unknown): string { + return displayValue(value); +} + +function truncateOneLine(value: string, maxChars: number): string { + const oneLine = value.replace(/\s+/gu, " ").trim(); + return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 3))}...`; +} + +function truncateMultiline(value: string, maxChars: number): string { + return value.length <= maxChars ? value : `${value.slice(0, maxChars)}\n...[truncated ${value.length - maxChars} chars; rerun with --full-text or --full]`; +} + +function relativeAge(iso: string | null): string { + if (iso === null) return "-"; + const ms = Date.now() - Date.parse(iso); + if (!Number.isFinite(ms) || ms < 0) return "-"; + const minutes = Math.floor(ms / 60000); + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 48) return `${hours}h`; + return `${Math.floor(hours / 24)}d`; +} + +function pickCompact(value: Record, keys: string[]): Record { + const picked: Record = {}; + for (const key of keys) { + if (value[key] !== undefined) picked[key] = value[key]; + } + return picked; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + interface TriggerOptions { confirm: boolean; dryRun: boolean; @@ -1606,9 +2471,47 @@ interface PreparedAgentRunCliArgs { } type AgentRunOfficialCliBridgeGroup = "queue" | "sessions" | "aipod-specs" | "aipods" | "runs" | "commands" | "runner"; +type AgentRunResourceVerb = "get" | "describe" | "events" | "logs" | "result" | "ack" | "cancel" | "create" | "apply" | "steer" | "send" | "explain"; +type AgentRunResourceKind = "task" | "run" | "command" | "runnerjob" | "session" | "aipodspec"; +type AgentRunOutputMode = "human" | "wide" | "name" | "json" | "yaml"; + +interface AgentRunResourceRef { + kind: AgentRunResourceKind; + name: string; +} + +interface AgentRunResourceOptions { + output: AgentRunOutputMode; + full: boolean; + raw: boolean; + debug: boolean; + limit: number; + queue: string | null; + state: string | null; + unread: boolean; + readerId: string; + taskId: string | null; + runId: string | null; + commandId: string | null; + sessionId: string | null; + afterSeq: number | null; + tail: number | null; + fullText: boolean; + reason: string | null; + dryRun: boolean; + file: string | null; + aipod: string | null; + idempotencyKey: string | null; + promptStdin: boolean; + passthroughArgs: string[]; +} async function runOfficialAgentRunCli(config: UniDeskConfig, group: AgentRunOfficialCliBridgeGroup, args: string[]): Promise> { const prepared = prepareOfficialAgentRunCliArgs([group, ...args]); + return await runPreparedOfficialAgentRunCli(config, prepared); +} + +async function runPreparedOfficialAgentRunCli(config: UniDeskConfig, prepared: PreparedAgentRunCliArgs): Promise> { const command = `agentrun ${prepared.args.join(" ")}`.trim(); const script = officialAgentRunCliScript(prepared); const result = await capture(config, g14SourceRoute, ["script", "--", script]); @@ -2212,11 +3115,19 @@ function shQuote(value: string): string { return `'${value.replace(/'/gu, "'\\''")}'`; } -function unsupported(args: string[]): Record { - return { - ok: false, - command: `agentrun ${args.join(" ")}`.trim(), - degradedReason: "unsupported-command", - message: "supported commands: agentrun aipod-specs list|show|render|apply|delete; agentrun queue submit|list|show|stats|commander|read|cancel|dispatch|refresh; agentrun runs show|events|result; agentrun commands show|result; agentrun runner jobs|job-status; agentrun sessions ps|show|turn|steer|cancel|output|trace|read; agentrun control-plane status|trigger-current|refresh; agentrun git-mirror status|sync|flush", - }; +function unsupported(args: string[]): RenderedCliResult { + const command = `agentrun ${args.join(" ")}`.trim(); + return renderedCliResult(false, command, [ + `Error: unsupported AgentRun command: ${command}`, + "", + "Supported resource commands:", + " agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send", + "", + "Compatibility bridge groups:", + " agentrun aipod-specs|queue|runs|commands|runner|sessions", + "", + "Operations:", + " agentrun control-plane status|trigger-current|refresh", + " agentrun git-mirror status|sync|flush", + ].join("\n")); } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 927d23ec..4e1bd5d5 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -57,7 +57,7 @@ export function rootHelp(): unknown { { command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr", description: "Host Codex commander skeleton contract, no-daemon smoke plan, dry-run approval preview, and advisory GPT-5.5 PR prompt boundary lint without live bridges, message sends, or submit gating." }, { command: "hwlab nodes control-plane|git-mirror|secret --node G14 --lane v03", description: "Manage HWLAB node/lane runtime prerequisites for v0.3+ with the node identity passed as data instead of a command family." }, { 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 aipod-specs|queue|runs|commands|runner|sessions|control-plane|git-mirror", description: "Use AgentRun v0.1 AipodSpec, Queue, Run, Command, read-only runner status, and Sessions through the official G14 CLI bridge, plus bounded Tekton/Argo and git-mirror operations." }, + { command: "agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; legacy bridge groups remain available for raw compatibility." }, { command: "platform-infra sub2api plan|apply|status|validate|codex-pool", description: "Deploy Sub2API in G14 platform-infra, manage the YAML-controlled Codex upstream pool, expose the unified API through FRP when needed, and configure master ~/.codex without printing API keys." }, { command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." }, { command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." }, @@ -65,7 +65,7 @@ export function rootHelp(): unknown { { command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." }, { command: "codex deploy [--provider-id D601] [--timeout-ms N]", description: "Disabled legacy Code Queue deploy path; use the dev-only artifact consumer instead." }, { command: "codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]", description: "Dry-run lint a runner prompt for DEV test class read-only/live-read/live-mutating authorization without echoing prompt text or touching live services." }, - { command: "codex submit|steer|resume|queue create|queue merge|move", description: "Frozen legacy Code Queue write commands; use agentrun queue and agentrun sessions for new commander work. Historical codex task/tasks/output/read/unread/queues remain available for archive troubleshooting." }, + { command: "codex submit|steer|resume|queue create|queue merge|move", description: "Frozen legacy Code Queue write commands; use agentrun create/apply/steer/send for new commander work. Historical codex task/tasks/output/read/unread/queues remain available for archive troubleshooting." }, { command: "codex skills-sync --dry-run [--full]", description: "Inspect the controlled runner skills hostPath lifecycle contract without copying files, restarting services, reading secrets, or mutating live runner paths." }, { command: "codex execution-plane [--full|--raw]", description: "Read-only D601 native k3s Code Queue execution-plane inspection; compares formal deployments, deprecated Compose residuals, commit markers, pod digest, and mounted worktree HEAD." }, { command: "codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full|--raw]", description: "Read-only PR admission check with compact commander output by default; use --full or --raw to expand the full runtime preflight, tool, and observation payload." }, @@ -76,10 +76,10 @@ export function rootHelp(): unknown { { command: "codex read ", description: "Mark one reviewed terminal task read and return terminal metadata plus final response; prompt/tool logs stay behind drill-down commands." }, { command: "codex dev-ready", description: "Fetch execution-container readiness, including sanitized skill injection status from /api/dev-ready." }, { command: "codex judge --attempt N [--dry-run] [--include-prompt]", description: "Replay one stored Code Queue attempt through the same judge context builder and MiniMax judge call path used by the live queue worker." }, - { command: "codex steer / codex resume ", description: "Frozen legacy execution mutation entries; use agentrun sessions steer/read/cancel/output/trace against AgentRun sessions instead." }, + { command: "codex steer / codex resume ", description: "Frozen legacy execution mutation entries; use agentrun steer/send/logs/events/result/ack/cancel against AgentRun resources instead." }, { command: "codex steer-confirm --steer-id [--raw]", description: "Read-only lookup for a steerId in task trace so deliveryUnconfirmed can be resolved without resending the corrective prompt." }, { command: "codex interrupt|cancel ", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." }, - { command: "codex queues [--full|--all] [--limit N] [--page N|--offset N]", description: "Read legacy Code Queue archive summaries. Legacy queue create/merge and move are frozen; use agentrun queue for new work." }, + { command: "codex queues [--full|--all] [--limit N] [--page N|--offset N]", description: "Read legacy Code Queue archive summaries. Legacy queue create/merge and move are frozen; use agentrun create/apply/get/cancel for new work." }, { command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page and progress summaries." }, { command: "job status [--tail-bytes N]", description: "Show job state with a structured progress summary and bounded stdout/stderr tails." }, { command: "job cancel ", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." }, @@ -385,11 +385,11 @@ function codexHelp(): unknown { usage: [ "bun scripts/cli.ts codex deploy # disabled legacy deployment entry", "bun scripts/cli.ts codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]", - "bun scripts/cli.ts agentrun queue commander --reader-id cli", - "bun scripts/cli.ts agentrun aipod-specs show Artificer", - "bun scripts/cli.ts agentrun queue submit --aipod Artificer --prompt-stdin", - "bun scripts/cli.ts agentrun queue submit --json-stdin <<'JSON'", - "bun scripts/cli.ts agentrun sessions trace --after-seq 0 --limit 100", + "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + "bun scripts/cli.ts agentrun describe aipodspec/Artificer", + "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", + "bun scripts/cli.ts agentrun apply -f - --dry-run", + "bun scripts/cli.ts agentrun logs session/ --tail 100", "bun scripts/cli.ts codex submit # frozen legacy write entry; returns legacy-code-queue-frozen", "bun scripts/cli.ts codex task [--detail] [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", "bun scripts/cli.ts codex tasks [--view commander|supervisor|full] [--queue id] [--status succeeded,running] [--unread|--unread-only] [--limit N] [--before-id id]", @@ -402,7 +402,7 @@ function codexHelp(): unknown { "bun scripts/cli.ts codex execution-plane [--full|--raw]", "bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full|--raw]", "bun scripts/cli.ts codex judge --attempt N [--dry-run] [--include-prompt]", - "bun scripts/cli.ts agentrun sessions steer --prompt-stdin", + "bun scripts/cli.ts agentrun steer session/ --prompt-stdin", "bun scripts/cli.ts codex steer # frozen legacy write entry", "bun scripts/cli.ts codex resume # frozen legacy write entry", "bun scripts/cli.ts codex steer-confirm --steer-id [--raw]", @@ -419,11 +419,11 @@ function codexHelp(): unknown { executionMode: { validModes: ["default", "windows-native"], boundary: "Legacy Code Queue submit is frozen; new runtime placement is selected by AgentRun Queue payloads.", - permissionVisibility: "Legacy Code Queue read commands expose historical runner metadata only; new tasks use AgentRun Queue/Sessions.", + permissionVisibility: "Legacy Code Queue read commands expose historical runner metadata only; new tasks use AgentRun resource primitives.", }, submitSummary: { default: "codex submit/enqueue now returns ok=false, frozen=true, degradedReason=legacy-code-queue-frozen and AgentRun replacement commands.", - replacement: "Use bun scripts/cli.ts agentrun queue submit --json-stdin with a quoted heredoc for new one-shot work; reserve --json-file for reusable reviewed files.", + replacement: "Use bun scripts/cli.ts agentrun apply -f - with a quoted YAML heredoc for structured work, or agentrun create task --aipod Artificer --prompt-stdin for one prompt task.", noDoubleWrite: "UniDesk does not mirror AgentRun submissions into old Code Queue and does not migrate old history into AgentRun.", rawDrillDown: "Use codex tasks/task/output/read/unread/queues only for legacy archive inspection.", }, @@ -453,10 +453,10 @@ function codexHelp(): unknown { }, examples: { promptLint: "bun scripts/cli.ts codex prompt-lint --prompt-file /tmp/code-queue-prompt.md", - agentRunCommander: "bun scripts/cli.ts agentrun queue commander --reader-id cli", - agentRunAipod: "bun scripts/cli.ts agentrun queue submit --aipod Artificer --prompt-stdin", - agentRunSubmit: "bun scripts/cli.ts agentrun queue submit --json-stdin <<'JSON'", - agentRunTrace: "bun scripts/cli.ts agentrun sessions trace --after-seq 0 --limit 100", + agentRunCommander: "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + agentRunAipod: "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", + agentRunSubmit: "bun scripts/cli.ts agentrun apply -f - --dry-run", + agentRunTrace: "bun scripts/cli.ts agentrun logs session/ --tail 100", frozenLegacySubmit: "bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md", }, disclosure: { @@ -482,7 +482,7 @@ function codexHelp(): unknown { embeddedIn: [], reference: "docs/reference/code-queue-supervision.md#dev-测试授权分级", }, - description: "Operate legacy Code Queue as a read-only archive through bounded task/output/read/unread/queues views. New task dispatch, retry/resume, steer, queue mutation, move, and workdir mutation are frozen and replaced by AgentRun Queue/Sessions via bun scripts/cli.ts agentrun queue|sessions.", + description: "Operate legacy Code Queue as a read-only archive through bounded task/output/read/unread/queues views. New task dispatch, retry/resume, steer, queue mutation, move, and workdir mutation are frozen and replaced by AgentRun resource primitives via bun scripts/cli.ts agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send.", }; } @@ -597,19 +597,19 @@ function artifactRegistryHelp(): unknown { function agentRunHelpSummary(): unknown { return { - command: "agentrun aipod-specs|queue|runs|commands|runner|sessions|control-plane|git-mirror", - output: "json", + command: "agentrun get|describe|events|logs|result|ack|cancel|create|apply|steer|send|control-plane|git-mirror", + output: "human by default; use -o json|yaml or --raw", usage: [ - "bun scripts/cli.ts agentrun aipod-specs show Artificer", - "bun scripts/cli.ts agentrun queue commander --reader-id cli", - "bun scripts/cli.ts agentrun queue show ", - "bun scripts/cli.ts agentrun runs show ", - "bun scripts/cli.ts agentrun commands show --run-id ", - "bun scripts/cli.ts agentrun runner jobs --run-id --command-id ", - "bun scripts/cli.ts agentrun sessions trace --after-seq 0 --limit 100", + "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", + "bun scripts/cli.ts agentrun describe task/", + "bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", + "bun scripts/cli.ts agentrun logs session/ --tail 100", + "bun scripts/cli.ts agentrun result run/ --command ", + "bun scripts/cli.ts agentrun ack task/", + "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", "bun scripts/cli.ts agentrun control-plane status", ], - description: "Operate AgentRun v0.1 AipodSpec, queue, run, command, read-only runner status, sessions, and G14 control-plane entrypoints with progressive disclosure: queue commander -> queue show -> runs/commands/runner/session detail.", + description: "Operate AgentRun v0.1 resources with Kubernetes-style verbs and progressive disclosure. Legacy queue/runs/commands/runner/sessions bridge groups remain for raw compatibility.", }; } diff --git a/scripts/src/output.ts b/scripts/src/output.ts index f1bc69af..7433ba1e 100644 --- a/scripts/src/output.ts +++ b/scripts/src/output.ts @@ -10,6 +10,13 @@ export interface JsonEnvelope { error?: unknown; } +export interface RenderedCliResult { + ok: boolean; + command: string; + renderedText: string; + contentType: "text/plain" | "application/json" | "application/yaml"; +} + const GH_OUTPUT_DUMP_THRESHOLD_BYTES = 20 * 1024; const OUTPUT_DUMP_PREVIEW_CHARS = 2000; const OUTPUT_DUMP_DIR = join(tmpdir(), "unidesk-cli-output"); @@ -33,6 +40,18 @@ export function emitJson(command: string, data: T, ok = true): void { safeStdoutWrite(renderEnvelope(command, envelope)); } +export function isRenderedCliResult(value: unknown): value is RenderedCliResult { + return typeof value === "object" + && value !== null + && typeof (value as { renderedText?: unknown }).renderedText === "string" + && typeof (value as { command?: unknown }).command === "string" + && typeof (value as { ok?: unknown }).ok === "boolean"; +} + +export function emitText(text: string): void { + safeStdoutWrite(text.endsWith("\n") ? text : `${text}\n`); +} + export function emitError(command: string, error: unknown): void { const payload = normalizeErrorPayload(command, error); const envelope: JsonEnvelope = { ok: false, command, error: payload };