diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3056ef6b..13dfed3a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -26,19 +26,21 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;`decision requirement list/upsert` 在同一 records 模型上管理 `goal|decision|blocker|debt|experiment` 需求记录。它们不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。 - `decision diary import ` 将带 `# YYYY年M月D日`、`# YYYY-MM-DD` 或 `# YYYY/M/D` 标题的工作日志拆成每天一篇 Markdown 日记,按 `YYYY-MM/YYYY-MM-DD.md` 虚拟路径写入 Decision Center PostgreSQL;`decision diary list/months/show` 分别用于按月/日期查询、列出月份和查看单日正文;`decision diary edit|upsert --body-file [--title text] [--source-file path] [--tag tag]` 通过 `PUT /api/diary/entries/:idOrDate` 创建当天或历史条目并编辑既有条目。 - `deploy check/plan/apply` 默认从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新已支持目标;`deploy plan --env dev|prod` 只从 `origin/master:deploy.json#environments.` 读取 manifest 并输出 dry-run 环境计划,不使用本地 dirty worktree;当前 `deploy apply --env dev` 支持 D601 `backend-core` target-side rollout,以及 `frontend`/`baidu-netdisk`/`decision-center` artifact consumers,`findjob`/`pipeline`/`met-nonlinear` 为 D601 direct Compose artifact consumers,`k3sctl-adapter` 只提供 plan/dry-run;dev desired-state smoke 使用 `ci run-dev-e2e`;规则见 `docs/reference/deploy.md`、`docs/reference/dev-environment.md` 和 `docs/reference/dev-ci-runner.md`。`deploy apply --env prod` 同时覆盖 `findjob` 和 `pipeline` 的 pull-only Compose CD,但 `met-nonlinear` 仍然只允许 dry-run/plan,`k3sctl-adapter` 只允许 plan/dry-run。 -- `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。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f `,仍不 apply 资源。 +- `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` 时额外执行 `kubectl apply --dry-run=client --validate=false -f `,仍不 apply 资源。 - `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`。 - `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`。 - `codex deploy ` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 - `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 submit 把低内存主机或 `code-queue-mgr` 控制面打抖;返回值会附带 `submitConcurrencyGuard` 说明本次提交的锁与等待信息。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 -- `codex tasks [--queue id] [--limit N] [--unread-only]` 通过同一私有代理输出一个只读聚合视图,按 `running`、`completedUnread`、`recentCompleted` 三个 section 汇总当前需要盯的任务;每个条目都带 `taskId`、`queueId`、`status`、`currentAttempt`、`updatedAt`、`finishedAt`、`unread`/`unreadTerminal`、`lastAssistantMessage` 摘要和可直接复制的 `commands.show` / `commands.trace`。`--queue` 限定单个队列,`--limit` 控制各 section 的最大条数,`--unread-only` 只保留未读终态和正在运行的任务。 +- `codex tasks [--view supervisor|full] [--queue id] [--status succeeded|running|queued|failed|canceled|judging|retry_wait[,..]] [--unread|--unread-only] [--limit N] [--before-id id]` 通过同一私有代理输出渐进式披露视图。默认 `supervisor` 只返回 `running`、`completedUnread`、`recentCompleted`、`queued` 和 `executionDiagnostics` 摘要,不嵌入完整 Trace、final response 或全量 overview;每个条目都带 `commands.show`、`commands.trace`、`commands.output`、`commands.read` 和 `commands.full`。`--unread` 是 `--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败,不能静默忽略。需要完整当前页任务简表时显式使用 `--view full` 或 `--full`,仍受 `--limit` 和 `--before-id` 分页约束。 - `codex task --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`。 - `codex output --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。 +- `codex read ` 在人工审阅后标记单个终态任务已读;列表、overview 和 supervisor 视图只返回这个命令字段,不得自动执行,也不得批量清空未读状态。 +- `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 interrupt|cancel ` 通过 Code Queue 私有代理请求中断;running/judging 任务会请求 D601 当前 agent run 停止,queued/retry_wait 任务的取消也必须保持与 WebUI 相同代理路径,返回有界 task 摘要和后续查询命令。任何需要接触 active run 的动作仍属于 D601 执行面。 -- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues` 列表、`queue create ` 创建、`queue merge --into ` 合并、`move --queue ` 迁移;这些队列管理入口默认由主 server `code-queue-mgr` 直管 PostgreSQL,仍通过稳定 `code-queue` 用户服务代理路径访问。同一个 queue 内部串行执行,不同 queue 之间并行执行。迁移只允许尚未被 scheduler claim 的 `queued`/`retry_wait` 任务,必须满足 `startedAt=null`、`currentAttempt=0` 且没有 active thread/turn;已进入 `running`/`judging` 或已有 claim 标记的任务返回 409,不得被 move/merge 回写成 queued。合并会移动可迁移任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;若 source 或 target queue 存在 active/claimed 任务,合并整体返回 409。合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行,成功迁移 queued/retry_wait 任务后由 D601 scheduler 轮询推进。 +- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues [--full|--all]` 列表、`queue create ` 创建、`queue merge --into ` 合并、`move --queue ` 迁移;这些队列管理入口默认由主 server `code-queue-mgr` 直管 PostgreSQL,仍通过稳定 `code-queue` 用户服务代理路径访问。`codex queues` 默认只返回 active/nonempty/unread/runnable queue 摘要、全局 counts 和 execution diagnostics;完整队列数组必须显式 `--full` 或 `--all`。同一个 queue 内部串行执行,不同 queue 之间并行执行。迁移只允许尚未被 scheduler claim 的 `queued`/`retry_wait` 任务,必须满足 `startedAt=null`、`currentAttempt=0` 且没有 active thread/turn;已进入 `running`/`judging` 或已有 claim 标记的任务返回 409,不得被 move/merge 回写成 queued。合并会移动可迁移任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;若 source 或 target queue 存在 active/claimed 任务,合并整体返回 409。合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行,成功迁移 queued/retry_wait 任务后由 D601 scheduler 轮询推进。 - 所有 `codex` 查询和管理命令必须走与 WebUI 相同的 backend-core 私有代理路径 `/api/microservices/code-queue/proxy/...`;CLI 不得为了提交、移动、中断、取消或队列管理直接调用 D601 内部 Service、数据库、pod curl 或 k3sctl scheduler 子服务。若该路径失败,应先修复 CLI/backend/provider tunnel 链路,而不是绕过控制面。 - `job list [--limit N] [--include-command]` 与 `job status [--tail-bytes N]` 查询 `.state/jobs/` 文件系统状态,是异步命令的可观测入口。`job list` 默认只返回最新 50 条摘要;`job status` 默认只返回 stdout/stderr 末尾 12000 字节,并带 `tailPolicy` 与完整日志路径。 - `debug health`、`debug dispatch` 与 `debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程,只用于开发调试,不写入 `TEST.md` 的正式验收步骤。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 14cac356..d54daa5e 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -47,11 +47,16 @@ 常用入口: -- `bun scripts/cli.ts codex queues`:查看队列计数、active task id、完成未读任务和控制面诊断。 +- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认有界监督视图,包括 running、完成未读、最近完成、queued/runnable、execution diagnostics 和下一步 drill-down 命令。 +- `bun scripts/cli.ts codex queues`:查看低噪声队列计数、active task id、完成未读队列、runnable 队列和控制面诊断;只有需要完整队列行时才加 `--full`。 +- `bun scripts/cli.ts codex tasks --unread --limit N`:查看完成未读审阅积压;`--unread` 与 `--unread-only` 等价,不能被静默忽略。 +- `bun scripts/cli.ts codex tasks --status succeeded --unread --limit N`:按具体终态过滤监督结果;不支持的 status filter 必须显式失败,不能扩大为未过滤结果。 - `bun scripts/cli.ts codex task `:查看 attempt、最后 assistant message、最后错误、cancel flag 和当前状态。 - 当摘要不足时,再使用 `bun scripts/cli.ts codex task --trace --limit N` 或 `codex output`。 - 当 master 控制面状态和 D601 scheduler 状态看起来分裂时,使用 `docs/reference/observability.md` 中的活性规则判断。 +默认 supervisor 视图必须保持低噪声。每个任务应带 `commands.show`、`commands.trace`、`commands.output`、`commands.full` 和 `commands.read`,让下一步渐进披露动作明确;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。 + 队列诊断中的 `split-brain` 表示控制面/执行面观测分裂,不自动证明任务已经死亡。如果任务 heartbeat 新鲜且 trace 仍在推进,应把任务视为 live,继续监督,而不是 interrupt 或替换。队列摘要应显示 `effectiveLiveness=live`、`splitBrainLive=true` 和 `recommendedAction=continue-supervision`;如果 heartbeat expired/missing 或满足 stale-recovery 条件,应显示 `effectiveLiveness=at-risk`。 对于 trace 或 heartbeat 新鲜的长任务,通常应保持运行。每几分钟轮询一次优于反复 interrupt/retry。 @@ -60,7 +65,7 @@ 当任务离开 `running` 或 `judging` 后,其结果仍然是未读工作。指挥官必须检查 final response 和 judge 记录,然后再决定是否补充并发窗口。 -禁止在检查前用批量 read 动作清空完成未读任务。每个完成任务必须先单独审阅,再单独标记已读,使未读状态继续代表“仍需审阅”。 +禁止在检查前用批量 read 动作清空完成未读任务。每个完成任务必须先单独审阅,再用 `bun scripts/cli.ts codex read ` 单独标记已读,使未读状态继续代表“仍需审阅”。 ## 指挥工作流 diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 47f13563..e32c564b 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -200,6 +200,7 @@ D601 上必须显式使用原生 k3s kubeconfig:`KUBECONFIG=/etc/rancher/k3s/k - 期望状态部署:Code Queue 仍由 `deploy.json` 的 repo 与 commit 声明版本,但维护通道直连 D601 不能再用 `deploy apply --service code-queue` 或 `codex deploy ` 部署 Code Queue。未来受控 Code Queue CD 应复用 `docs/reference/deploy.md` 的 target-side build 规范在 D601 构建、导入 k3s、rollout 并验证 live commit,不得维护第二套部署语义。 - 更名与灾备恢复:旧版 Codex 队列服务名只允许作为兼容诊断和一次性迁移来源;`code-queue-backend` 容器自身 `/health` 正常但 `microservice health code-queue` 返回 provider 直连错误时,优先判定为 backend-core 仍加载旧 `MICROSERVICES_JSON` 或 adapter manifest 未刷新,必须刷新 `.state/docker-compose.env`、重建/替换 `backend-core` 与 `k3sctl-adapter`,随后用 `microservice list` 验证 `code-queue` 的 `runtime.orchestrator=k3sctl`、`backend.proxyMode=k3sctl-adapter-http` 和无业务容器直连摘要。正式 k3s 部署成功后,旧 direct Docker `code-queue-backend` 必须停止并移除,不能与 `code-queue-scheduler` 同时运行;否则会形成双 scheduler、双健康来源和错误的恢复判断。 - Codex 认证:容器必须从 D601 的 `/home/ubuntu/.codex/config.toml` 同步 Codex provider 配置到 D601 `.state/code-queue/codex-home/config.toml`,并只读挂载 `/home/ubuntu/.codex/auth.json` 到容器 `/root/.codex/auth.json` 后同步到 `.state/code-queue/codex-home/auth.json`,让 `codex app-server` 使用与 host 一致的 provider 登录态;同时通过 D601 `.state/code-queue-d601.env` 或 k8s `code-queue-env` secret 透传 `OPENAI_API_KEY`、`CRS_OAI_KEY` 等 provider 所需变量。这些 provider 环境变量和 auth 文件不得写入仓库,必须由 D601 运行时文件或 k8s secret 注入,确保容器重建和重启后不会丢失认证。新增 provider 的 `env_key` 时必须增加同类运行时透传和 Compose/k8s 持久化,禁止把 Codex 或 MiniMax 密钥写入仓库文件。Code Queue 容器必须只读挂载 D601 WSL host 的 SSH 目录到 `/root/.ssh`(默认 `/home/ubuntu/.ssh`),让容器内 `git push`、`ssh -T git@github.com` 与 WSL host 使用同一套 GitHub SSH key/known_hosts;不得把私钥复制进镜像或仓库。 +- Skill 注入边界:DEV Code Queue scheduler/read/write Pod 必须把宿主 `/home/ubuntu/.agents/skills` 只读挂载到容器 `/root/.agents/skills`,并设置 `UNIDESK_SKILLS_PATH=/root/.agents/skills`,让执行任务能读取 `cli-spec` 等技能;只允许挂载 skill 目录本身,不得把宿主 `~/.agents`、`~/.codex`、token、auth JSON 或其他隐私配置整体暴露给任务容器。`/health` 和 `/api/dev-ready` 必须暴露非敏感 `skills` 状态:路径、exists、available、readonly、skillCount、`cliSpecAvailable` 和修复建议;CLI `codex dev-ready` 可读取该摘要。当前交付只要求 DEV manifest 和旧 direct Compose 诊断路径具备只读 skill 注入;PROD Code Queue 发布前必须单独审查隔离级别,不能把 DEV 桥接模式直接推广为生产默认。 - Develop-ready 镜像:Code Queue 镜像必须在启动前预装 UniDesk/Pipeline 调试所需工具,至少包含 `codex`、`bun`、`node`、`npm`/`npx`、`git`、`rg`、`curl`、`python3`/`pip3`、`docker`、`docker compose`、`docker-compose`、`jq`、`ssh`、`rsync`、`make`、`gcc`/`g++`、`iptables`、`tar`、`gzip` 和 `unzip`;不得依赖 Codex 任务运行时再 `apt-get install` 这些基础环境。 - 远程开发容器与任务执行 Provider:Code Queue 必须能通过 live API 拉起 D601 等计算节点上的开发容器,入口为 `POST /api/dev-containers//start`,默认 Provider 为 `D601`。该流程由 Code Queue 调用 UniDesk `ssh ` 维护桥在目标节点创建 `unidesk-codex-dev-`,并在 Code Queue 所在节点与开发容器之间建立 `ssh -w` TUN 点对点链路;服务所在节点负责对开发容器的 TUN 源地址做 NAT/MASQUERADE,开发容器默认路由和 DNS 改走该 TUN,从而让 `ping google.com`、DNS、HTTP(S) 等出网都经主 server 全局代理,而不是依赖 D601 本地网络。提交 Code Queue 任务时必须支持选择执行 Provider:`D601` 在 D601 原生 k3s 的 active Code Queue scheduler/runner Pod 中本机执行,默认工作目录为 `/workspace`,并且 `/workspace` 必须映射 D601 WSL host 的 `/home/ubuntu`;同一个 hostPath 还必须挂载到容器内 `/home/ubuntu`,让 WSL home 里的绝对 symlink(例如 `/workspace/cq-deploy -> /home/ubuntu/unidesk-code-queue-deploy`)在任务中可解析,不能只看到 symlink 名而无法进入目标目录。`/root/unidesk` 与 `/app` 必须单独映射 `/home/ubuntu/cq-deploy` 作为服务部署仓库;其他 Provider 在对应 `unidesk-codex-dev-` 容器中执行,默认工作目录为 `/home/ubuntu`,可按任务覆盖 `cwd`。远程任务启动前必须自动复用或拉起该 Provider 的开发容器、同步 Codex 配置和允许的运行时 provider 环境变量,并通过同一 master TUN/NAT 链路出网;目标 host 存在 `/mnt` 时,开发容器必须挂载 host `/mnt:/mnt`,确保 D601 这类 WSL 节点的 Windows 盘符路径如 `/mnt/f/Work/ConStart` 在任务容器内可见,避免 agent 因缺少真实工作区而搜索到无关项目。TUN 建立必须幂等处理 stale 状态:启动前清理旧 `tun`、默认路由、旧 tunnel SSH 进程和旧 OUTPUT 跳转,缺失旧设备不能导致失败,冷启动运行时准备要有有界但足够的 timeout。TUN 建立后必须创建 `UD-CQ-EGRESS-` OUTPUT 链,规则只允许 loopback、既有连接、`tun` 出口以及到 master server 的 SSH tunnel 控制连接,随后 reject 其他 IPv4/IPv6 出站包;这条网络层封口是开发/执行容器的权威外网边界,不能用 `HTTP_PROXY`/`NO_PROXY` 环境变量替代,容器镜像也必须使用已解析出的唯一 `unidesk-code-queue:` 或显式 `image`,缺失时直接失败,禁止 provider-gateway image、`latest` 或其他隐式镜像 fallback。验收必须保留三类日志:容器建隧道后 `ping google.com` 成功、强制指定原 Docker 网卡直连外网被 `sealed_direct_ping=blocked_expected` 拦截、服务所在节点上对应 `UNIDESK-CODEX-DEV-` NAT 链或 `tun` 计数在 ping 前后增长;涉及 WSL 工作区任务时还必须在开发容器内验证目标 `/mnt/...` 路径可读。`GET /api/dev-containers//status` 必须展示默认路由、`route_8_8_8_8`、`egressFirewallChain` 和 OUTPUT 链跳转。开发容器代理密钥只生成到 `.state/code-queue/dev-proxy/` 与目标节点用户目录,不得提交到仓库。 - 远程维护桥调用:Code Queue 已迁移到 D601 后,Code Queue 后端 Pod 内没有主 server 的 `unidesk-backend-core` 容器,不能再把 `bun scripts/cli.ts ssh ...` 实现为本地 `docker exec unidesk-backend-core`。Code Queue 后端发起的 provider 维护命令必须通过主 server frontend `/api/dispatch` 进入 backend-core,再由目标 provider-gateway 执行 `host.ssh`;需要传递脚本时必须使用 base64 临时文件,超过 Host SSH 单命令长度上限时分块上传到目标 `/tmp` 后再执行,避免恢复到本地 Docker broker、交互 stdin 或手工 shell fallback。 diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index ebdeb87d..e178627a 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -60,7 +60,10 @@ interface CompactTaskMutationResponseOptions { interface CodexTasksOptions { queueId: string | undefined; limit: number; + beforeId: string | undefined; unreadOnly: boolean; + statusFilter: string[] | null; + view: "supervisor" | "full"; } interface CodexTasksEntry { @@ -73,10 +76,15 @@ interface CodexTasksEntry { readAt: string | null; unread: boolean; unreadTerminal: boolean; + promptPreview: Record; + queuedReason: Record | null; lastAssistantMessage: Record | null; commands: { show: string; trace: string; + output: string; + read: string; + full: string; }; } @@ -84,15 +92,26 @@ interface CodexTasksSection { count: number; returned: number; truncated: boolean; + hasMore: boolean; + commands: { + next: string | null; + full: string; + }; items: CodexTasksEntry[]; } interface CodexTasksDegraded { summaryFetchFailedTaskIds: string[]; summaryFetchErrorCount: number; + summaryFetchOmittedTaskCount: number; reason: string; } +interface CodexQueuesOptions { + full: boolean; + limit: number; +} + type CodexRequestInit = { method?: string; body?: unknown }; type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown; type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise; @@ -300,6 +319,23 @@ function hasFlag(args: string[], name: string): boolean { return args.includes(name); } +function assertKnownOptions(args: string[], spec: { flags?: string[]; valueOptions?: string[] }, command: string): void { + const flags = new Set(spec.flags ?? []); + const valueOptions = new Set(spec.valueOptions ?? []); + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] ?? ""; + if (!arg.startsWith("--")) continue; + if (flags.has(arg)) continue; + if (valueOptions.has(arg)) { + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); + index += 1; + continue; + } + throw new Error(`unsupported ${command} option: ${arg}`); + } +} + function textView(text: string, full: boolean, maxChars: number): Record { const truncated = !full && text.length > maxChars; return { @@ -324,6 +360,21 @@ function compactLastAssistant(value: unknown, full: boolean): Record | null { + const record = asRecord(value); + if (record === null) return null; + return { + code: record.code ?? null, + label: record.label ?? null, + message: textPreview(asString(record.message), 280), + blockerTaskId: record.blockerTaskId ?? null, + blockerQueueId: record.blockerQueueId ?? null, + waitPosition: record.waitPosition ?? null, + activeRunSlotCount: record.activeRunSlotCount ?? null, + maxActiveQueues: record.maxActiveQueues ?? null, + }; +} + function compactSchedulerHeartbeat(value: unknown): Record | null { const record = asRecord(value); if (record === null) return null; @@ -667,6 +718,10 @@ function compactTracePage(body: Record, taskId: string, limit: } function parseTaskOptions(args: string[]): CodexTaskOptions { + assertKnownOptions(args, { + flags: ["--trace", "--tail", "--from-start", "--first", "--full", "--raw-summary"], + valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--trace-limit", "--limit", "--tool-limit"], + }, "codex task"); const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]); const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0); const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first"); @@ -685,6 +740,10 @@ function parseTaskOptions(args: string[]): CodexTaskOptions { } function parseOutputOptions(args: string[]): CodexOutputOptions { + assertKnownOptions(args, { + flags: ["--tail", "--from-start", "--first", "--full-text", "--raw"], + valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--limit", "--max-text-chars"], + }, "codex output"); const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]); const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0); const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first"); @@ -700,14 +759,47 @@ function parseOutputOptions(args: string[]): CodexOutputOptions { } function parseTasksOptions(args: string[]): CodexTasksOptions { + assertKnownOptions(args, { + flags: ["--unread-only", "--unread", "--full", "--all"], + valueOptions: ["--queue", "--queue-id", "--limit", "--status", "--view", "--before-id", "--beforeId"], + }, "codex tasks"); + const viewValue = optionValue(args, ["--view"]) ?? "supervisor"; + if (viewValue !== "supervisor" && viewValue !== "full") throw new Error(`--view must be supervisor or full; got ${viewValue}`); + const statusRaw = optionValue(args, ["--status"]); + const statusFilter = statusRaw === undefined + ? null + : statusRaw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean); + if (statusFilter !== null) { + const supported = new Set(["queued", "running", "judging", "retry_wait", "succeeded", "failed", "canceled"]); + const unsupported = statusFilter.filter((status) => !supported.has(status)); + if (unsupported.length > 0) throw new Error(`unsupported --status value: ${unsupported.join(", ")}; supported: ${Array.from(supported).join(", ")}`); + } return { queueId: optionValue(args, ["--queue", "--queue-id"]), limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit), - unreadOnly: hasFlag(args, "--unread-only"), + beforeId: optionValue(args, ["--before-id", "--beforeId"]), + unreadOnly: hasFlag(args, "--unread-only") || hasFlag(args, "--unread"), + statusFilter, + view: hasFlag(args, "--full") || hasFlag(args, "--all") ? "full" : viewValue, + }; +} + +function parseQueuesOptions(args: string[]): CodexQueuesOptions { + assertKnownOptions(args, { + flags: ["--full", "--all"], + valueOptions: ["--limit"], + }, "codex queues"); + return { + full: hasFlag(args, "--full") || hasFlag(args, "--all"), + limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit), }; } function parseJudgeOptions(args: string[]): CodexJudgeOptions { + assertKnownOptions(args, { + flags: ["--dry-run", "--no-call", "--include-prompt"], + valueOptions: ["--attempt", "--attempt-id", "--attemptIndex"], + }, "codex judge"); const rawAttempt = optionValue(args, ["--attempt", "--attempt-id", "--attemptIndex"]) ?? positionalArgs(args)[0]; let attempt: number | null = null; if (rawAttempt !== undefined) { @@ -823,7 +915,7 @@ function isTerminalTaskStatus(status: unknown): boolean { } function isActiveTaskStatus(status: unknown): boolean { - return status === "running" || status === "judging" || status === "retry_wait" || status === "queued"; + return status === "running" || status === "judging"; } function taskStatusRank(status: unknown): number { @@ -855,16 +947,24 @@ function taskUnreadTerminal(task: Record): boolean { return isTerminalTaskStatus(status) && (readAt === null || readAt === undefined || readAt === ""); } +function taskQueuedRunnable(task: Record): boolean { + const status = asString(task.status); + if (status === "queued" || status === "retry_wait") return true; + return asRecord(task.queuedReason)?.code === "ready"; +} + function taskWatchEntry(task: Record, summary: Record | null): CodexTasksEntry { const taskId = asString(task.id); const summaryCommands = summary === null ? null : asRecord(summary.commands); const summaryLastAssistant = summary?.lastAssistantMessage ?? task.lastAssistantMessage; + const promptPreview = textPreview(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), 360); const showCommand = typeof summary?.cliHint === "string" && summary.cliHint.length > 0 ? summary.cliHint : `bun scripts/cli.ts codex task ${taskId}`; const traceCommand = typeof summary?.traceHint === "string" && summary.traceHint.length > 0 ? summary.traceHint : `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`; + const outputCommand = `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`; return { taskId, queueId: asString(task.queueId) || null, @@ -875,31 +975,51 @@ function taskWatchEntry(task: Record, summary: Record 0 ? summaryCommands.show : showCommand, trace: typeof summaryCommands?.trace === "string" && summaryCommands.trace.length > 0 ? summaryCommands.trace : traceCommand, + output: outputCommand, + read: `bun scripts/cli.ts codex read ${taskId}`, + full: `bun scripts/cli.ts codex task ${taskId} --full`, }, }; } -function buildTaskWatchSection(tasks: Record[], summaries: Map>, limit: number): CodexTasksSection { +function buildTaskWatchSection( + tasks: Record[], + summaries: Map>, + limit: number, + nextCommand: string | null, + fullCommand: string, +): CodexTasksSection { const visibleTasks = tasks.slice(0, limit); const items = visibleTasks.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null)); + const truncated = tasks.length > limit; return { count: tasks.length, returned: items.length, - truncated: tasks.length > limit, + truncated, + hasMore: truncated, + commands: { + next: truncated ? nextCommand : null, + full: fullCommand, + }, items, }; } -function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>): CodexTasksDegraded | null { - if (summaryErrors.length === 0) return null; +function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>, omittedTaskCount = 0): CodexTasksDegraded | null { + if (summaryErrors.length === 0 && omittedTaskCount === 0) return null; return { summaryFetchFailedTaskIds: summaryErrors.map((error) => error.taskId), summaryFetchErrorCount: summaryErrors.length, - reason: "task summary fetch failed for one or more entries; unread state still comes from task-level overview data", + summaryFetchOmittedTaskCount: omittedTaskCount, + reason: summaryErrors.length > 0 + ? "task summary fetch failed for one or more entries; unread state still comes from task-level overview data" + : "task summary fetch was intentionally omitted for bounded full view; use commands.show for per-task detail", }; } @@ -925,10 +1045,46 @@ function sortCompletedWatchTasks(tasks: Record[]): Record[]): Record[] { + return [...tasks] + .filter((task) => taskQueuedRunnable(task)) + .sort((left, right) => { + const rankDelta = taskStatusRank(asString(left.status)) - taskStatusRank(asString(right.status)); + if (rankDelta !== 0) return rankDelta; + const timeDelta = taskTimelineMs(left) - taskTimelineMs(right); + if (timeDelta !== 0) return timeDelta; + return asString(left.id).localeCompare(asString(right.id)); + }); +} + +function taskMatchesStatusFilter(task: Record, statusFilter: string[] | null): boolean { + return statusFilter === null || statusFilter.includes(asString(task.status)); +} + +function filterTasksForOptions(tasks: Record[], options: CodexTasksOptions): Record[] { + return tasks + .filter((task) => options.queueId === undefined || asString(task.queueId) === options.queueId) + .filter((task) => taskMatchesStatusFilter(task, options.statusFilter)) + .filter((task) => !options.unreadOnly || taskUnreadTerminal(task)); +} + +function taskListCommand(options: CodexTasksOptions, extra: string[] = []): string { + const args = ["codex", "tasks"]; + if (options.view !== "supervisor") args.push("--view", options.view); + if (options.queueId !== undefined) args.push("--queue", options.queueId); + if (options.unreadOnly) args.push("--unread"); + if (options.statusFilter !== null) args.push("--status", options.statusFilter.join(",")); + if (options.limit !== defaultTasksLimit) args.push("--limit", String(options.limit)); + if (options.beforeId !== undefined) args.push("--before-id", options.beforeId); + args.push(...extra); + return `bun scripts/cli.ts ${args.join(" ")}`; +} + function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { summaries: Map>; degraded: CodexTasksDegraded | null } { + const boundedIds = taskIds.slice(0, maxTasksLimit); const summaries = new Map>(); const errors: Array<{ taskId: string; message: string }> = []; - for (const taskId of taskIds) { + for (const taskId of boundedIds) { try { const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`))); const summary = asRecord(response.body.summary) ?? {}; @@ -937,13 +1093,14 @@ function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { errors.push({ taskId, message: error instanceof Error ? error.message : String(error) }); } } - return { summaries, degraded: collectTaskWatchDegraded(errors) }; + return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) }; } async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexResponseFetcher): Promise<{ summaries: Map>; degraded: CodexTasksDegraded | null }> { + const boundedIds = taskIds.slice(0, maxTasksLimit); const summaries = new Map>(); const errors: Array<{ taskId: string; message: string }> = []; - await Promise.all(taskIds.map(async (taskId) => { + await Promise.all(boundedIds.map(async (taskId) => { try { const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`))); const summary = asRecord(response.body.summary) ?? {}; @@ -952,7 +1109,7 @@ async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexRes errors.push({ taskId, message: error instanceof Error ? error.message : String(error) }); } })); - return { summaries, degraded: collectTaskWatchDegraded(errors) }; + return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) }; } type CodexTasksTaskPage = { @@ -966,6 +1123,7 @@ function tasksListQueryString(options: CodexTasksOptions): string { limit: 200, priorityLimit: 200, queueId: options.queueId, + beforeId: options.beforeId, includeActive: 1, selected: 0, compact: 1, @@ -1000,21 +1158,32 @@ function codexTasksOverviewResult( summaries: Map>, degraded: CodexTasksDegraded | null, ): Record { - const allTasks = options.queueId === undefined ? taskPage.tasks : taskPage.tasks.filter((task) => asString(task.queueId) === options.queueId); + if (options.view === "full") return codexTasksFullResult(taskPage, upstream, options, summaries, degraded); + const allTasks = filterTasksForOptions(taskPage.tasks, options); const runningTasks = sortRunningWatchTasks(allTasks); const unreadCompletedTasks = sortCompletedWatchTasks(allTasks).filter((task) => taskUnreadTerminal(task)); const recentCompletedTasks = options.unreadOnly ? [] : sortCompletedWatchTasks(allTasks); - const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit); - const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit); - const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit); + const queuedTasks = options.unreadOnly ? [] : sortQueuedWatchTasks(allTasks); + const nextBeforeId = asString(taskPage.pagination.nextBeforeId) || null; + const sourceHasMore = asBoolean(taskPage.pagination.hasMore); + const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null; + const fullCommand = taskListCommand({ ...options, view: "full" }); + const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit, nextCommand, fullCommand); + const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand); + const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit, nextCommand, fullCommand); + const queuedSection = buildTaskWatchSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand); const pagination = taskPage.pagination; + const diagnostics = compactExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics); return { upstream, - overview: { + supervisor: { filters: { + view: options.view, queueId: options.queueId ?? null, limit: options.limit, unreadOnly: options.unreadOnly, + status: options.statusFilter, + beforeId: options.beforeId ?? null, }, source: { endpoint: "/api/tasks/overview", @@ -1026,29 +1195,92 @@ function codexTasksOverviewResult( nextBeforeId: asString(pagination.nextBeforeId) || null, includeActive: asBoolean(pagination.includeActive), }, - queue: taskPage.queue, + bounded: true, + disclosure: { + defaultView: "supervisor", + fullCommand, + next: nextCommand, + rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`, + }, counts: { scanned: allTasks.length, running: runningSection.count, completedUnread: unreadSection.count, recentCompleted: recentSection.count, + queued: queuedSection.count, }, + executionDiagnostics: diagnostics, degraded, commands: { - refresh: `bun scripts/cli.ts codex tasks${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.unreadOnly ? " --unread-only" : ""}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`, + refresh: taskListCommand(options), + unread: taskListCommand({ ...options, unreadOnly: true }), + byStatus: `bun scripts/cli.ts codex tasks --status ${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`, + full: fullCommand, + next: nextCommand, }, running: runningSection, completedUnread: unreadSection, recentCompleted: recentSection, + queued: queuedSection, + }, + }; +} + +function codexTasksFullResult( + taskPage: CodexTasksTaskPage, + upstream: { ok: unknown; status: unknown }, + options: CodexTasksOptions, + summaries: Map>, + degraded: CodexTasksDegraded | null, +): Record { + const filtered = filterTasksForOptions(taskPage.tasks, options); + const visible = filtered.slice(0, options.limit); + const pagination = taskPage.pagination; + const nextBeforeId = asString(pagination.nextBeforeId) || null; + const sourceHasMore = asBoolean(pagination.hasMore); + const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null; + return { + upstream, + tasks: { + filters: { + view: "full", + queueId: options.queueId ?? null, + limit: options.limit, + unreadOnly: options.unreadOnly, + status: options.statusFilter, + beforeId: options.beforeId ?? null, + }, + source: { + endpoint: "/api/tasks/overview", + returned: asNumber(pagination.returned, 0) || null, + total: asNumber(pagination.total, 0) || null, + hasMore: asBoolean(pagination.hasMore), + nextBeforeId, + }, + count: filtered.length, + returned: visible.length, + hasMore: filtered.length > visible.length || sourceHasMore, + degraded, + commands: { + supervisor: taskListCommand({ ...options, view: "supervisor" }), + next: nextCommand, + rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`, + }, + items: visible.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null)), }, }; } function visibleTaskIdsForOverview(tasks: Record[], options: CodexTasksOptions): string[] { + const filtered = filterTasksForOptions(tasks, options); + if (options.view === "full") { + return filtered.slice(0, options.limit).map((task) => taskOverviewCandidateKey(task)).filter((taskId) => taskId.length > 0); + } return Array.from(new Set([ - ...sortRunningWatchTasks(tasks).slice(0, options.limit), - ...sortCompletedWatchTasks(tasks).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit), - ...sortCompletedWatchTasks(tasks).slice(0, options.limit), + ...sortRunningWatchTasks(filtered).slice(0, options.limit), + ...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit), + ...sortCompletedWatchTasks(filtered).slice(0, options.limit), + ...sortQueuedWatchTasks(filtered).slice(0, options.limit), ].map((task) => taskOverviewCandidateKey(task)))) .filter((taskId) => taskId.length > 0); } @@ -1211,15 +1443,77 @@ function requireMergeTargetQueueId(args: string[], command: string): string { return raw.trim(); } -function codeQueues(): unknown { - const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"))); +function compactQueueRow(value: unknown): Record { + const record = asRecord(value) ?? {}; return { - upstream: response.upstream, - queues: response.body.queues ?? [], - queue: compactQueueMutationSummary(response.body.queue), + id: record.id ?? null, + name: record.name ?? null, + total: record.total ?? null, + counts: record.counts ?? {}, + unreadTerminal: record.unreadTerminal ?? 0, + activeTaskId: record.activeTaskId ?? null, + runnableTaskId: record.runnableTaskId ?? null, + processing: record.processing ?? null, + updatedAt: record.updatedAt ?? null, + commands: { + tasks: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --limit ${defaultTasksLimit}`, + unread: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --unread --limit ${defaultTasksLimit}`, + }, }; } +function compactQueuesResponse(body: Record, options: CodexQueuesOptions, upstream: { ok: unknown; status: unknown }): Record { + const queue = asRecord(body.queue) ?? asRecord(body.summary) ?? {}; + const queues = asArray(body.queues).map(compactQueueRow); + const activeIds = stringList(queue.activeQueueIds); + const nonemptyQueues = queues.filter((row) => asNumber(row.total, 0) > 0); + const unreadQueues = queues.filter((row) => asNumber(row.unreadTerminal, 0) > 0); + const runnableQueues = queues.filter((row) => row.runnableTaskId !== null && row.runnableTaskId !== undefined); + const activeQueues = queues.filter((row) => typeof row.id === "string" && activeIds.includes(row.id)); + const selected = options.full ? queues : Array.from(new Map([...activeQueues, ...unreadQueues, ...runnableQueues, ...nonemptyQueues].map((row) => [String(row.id), row])).values()); + const visible = selected.slice(0, options.limit); + const diagnostics = compactExecutionDiagnostics(queue.executionDiagnostics); + return { + upstream, + queues: { + view: options.full ? "full" : "summary", + bounded: true, + count: selected.length, + returned: visible.length, + hasMore: selected.length > visible.length, + totals: { + totalTasks: queue.total ?? null, + queueCount: queue.queueCount ?? queues.length, + activeQueueCount: activeIds.length, + nonemptyQueueCount: nonemptyQueues.length, + unreadQueueCount: unreadQueues.length, + runnableQueueCount: runnableQueues.length, + }, + activeQueueIds: queue.activeQueueIds ?? [], + activeTaskIds: queue.activeTaskIds ?? [], + queuedTaskIds: queue.queuedTaskIds ?? [], + counts: queue.counts ?? {}, + unreadTerminal: queue.unreadTerminal ?? 0, + executionDiagnostics: diagnostics, + items: visible, + commands: { + refresh: `bun scripts/cli.ts codex queues${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`, + full: `bun scripts/cli.ts codex queues --full${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`, + tasks: `bun scripts/cli.ts codex tasks --view supervisor --limit ${Math.min(options.limit, defaultTasksLimit)}`, + unread: `bun scripts/cli.ts codex tasks --unread --limit ${Math.min(options.limit, defaultTasksLimit)}`, + raw: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw", + }, + }, + }; +} + +function codeQueues(optionArgs: string[] = []): unknown { + const options = parseQueuesOptions(optionArgs); + const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"))); + if (options.full) return { upstream: response.upstream, queues: response.body.queues ?? [], queue: compactQueueMutationSummary(response.body.queue), commands: { summary: "bun scripts/cli.ts codex queues" } }; + return compactQueuesResponse(response.body, options, response.upstream); +} + function codexCreateQueue(queueId: string): unknown { return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } })); } @@ -1275,6 +1569,27 @@ function referenceTaskIdsFromOptions(args: string[]): string[] { } function parseSubmitOptions(args: string[]): CodexSubmitOptions { + assertKnownOptions(args, { + flags: ["--prompt-stdin", "--stdin", "--dry-run"], + valueOptions: [ + "--prompt-file", + "--file", + "--queue", + "--queue-id", + "--provider-id", + "--provider", + "--cwd", + "--workdir", + "--model", + "--reasoning-effort", + "--execution-mode", + "--mode", + "--max-attempts", + "--reference-task-id", + "--reference", + "--ref", + ], + }, "codex submit"); const maxAttempts = args.some((arg) => arg === "--max-attempts") ? positiveIntegerOption(args, ["--max-attempts"], 99, 99) : undefined; @@ -1339,6 +1654,19 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation }; } +function codexReadTask(taskId: string): unknown { + const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/read`), { method: "POST", body: {} })); + return { + upstream: response.upstream, + task: compactTaskMutationResponse(response.body.task), + queue: compactQueueMutationSummary(response.body.queue), + commands: { + show: `bun scripts/cli.ts codex task ${taskId}`, + unread: `bun scripts/cli.ts codex tasks --unread --limit ${defaultTasksLimit}`, + }, + }; +} + function compactQueueMutationSummary(value: unknown): Record | null { const record = asRecord(value); if (record === null) return null; @@ -1355,6 +1683,42 @@ function compactQueueMutationSummary(value: unknown): Record | }; } +function compactSkillsStatus(value: unknown): Record | null { + const record = asRecord(value); + if (record === null) return null; + return { + path: record.path ?? null, + mountPoint: record.mountPoint ?? null, + exists: record.exists ?? false, + available: record.available ?? false, + readonly: record.readonly ?? false, + skillCount: record.skillCount ?? 0, + cliSpecAvailable: record.cliSpecAvailable ?? false, + repairHint: record.repairHint ?? null, + }; +} + +function codeQueueDevReady(): unknown { + const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/dev-ready"))); + const devReady = asRecord(response.body.devReady) ?? {}; + return { + upstream: response.upstream, + devReady: { + ok: devReady.ok ?? false, + missingTools: devReady.missingTools ?? [], + workdir: devReady.workdir ?? null, + docker: devReady.docker ?? null, + codexConfig: devReady.codexConfig ?? null, + ssh: devReady.ssh ?? null, + skills: compactSkillsStatus(devReady.skills), + }, + commands: { + health: "bun scripts/cli.ts codex dev-ready", + raw: "bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw", + }, + }; +} + function codexSubmitTask(args: string[]): unknown { const options = parseSubmitOptions(args); const payload = submitPayload(options); @@ -1399,6 +1763,10 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[] if (action === "tasks" || action === "overview") { return codexTasksQuery(args.slice(1)); } + if (action === "dev-ready" || action === "health") { + assertKnownOptions(args.slice(1), {}, `codex ${action}`); + return codeQueueDevReady(); + } if (action === "output") { const taskId = requireTaskId(taskIdArg, "codex output"); return codexOutputQuery(taskId, args.slice(2)); @@ -1407,10 +1775,14 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[] const taskId = requireTaskId(taskIdArg, "codex judge"); return codexJudgeQuery(taskId, args.slice(2)); } - if (action === "queues") return codeQueues(); + if (action === "read" || action === "mark-read") { + const taskId = requireTaskId(taskIdArg, `codex ${action}`); + return codexReadTask(taskId); + } + if (action === "queues") return codeQueues(args.slice(1)); if (action === "queue") { const sub = taskIdArg ?? "list"; - if (sub === "list") return codeQueues(); + if (sub === "list") return codeQueues(args.slice(2)); if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "queue create")); if (sub === "merge") { const mergeArgs = args.slice(2); @@ -1425,5 +1797,5 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[] const taskId = requireTaskId(taskIdArg, `codex ${action}`); return codexInterruptTask(taskId); } - throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel"); + throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, queues, queue list, queue create, queue merge, move, interrupt, cancel"); } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 7eb6b097..8a91a2f3 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -44,11 +44,13 @@ export function rootHelp(): unknown { { 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 submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." }, { command: "codex task [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." }, - { command: "codex tasks [--queue id] [--limit N] [--unread-only]", description: "Show the current running, unread terminal, and recent completed Code Queue tasks in one JSON view." }, + { command: "codex tasks [--view supervisor|full] [--queue id] [--status status[,status]] [--unread|--unread-only] [--limit N] [--before-id id]", description: "Show the bounded supervisor view by default: running, unread terminal, recent completed, queued, diagnostics, and drill-down commands." }, { command: "codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." }, + { command: "codex read ", description: "Mark one reviewed terminal task read; never run automatically as part of listing." }, + { 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 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 | queue create | queue merge --into | move --queue )", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and deletes the source queue record." }, + { command: "codex (queues [--full|--all] | queue create | queue merge --into | move --queue )", description: "List low-noise queue summaries by default; full queue rows require --full/--all." }, { command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page." }, { command: "job status [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." }, { command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." }, @@ -186,17 +188,19 @@ function scheduleHelp(): unknown { function codexHelp(): unknown { return { - command: "codex deploy|submit|task|tasks|output|judge|interrupt|cancel|queues|queue|move", + command: "codex deploy|submit|task|tasks|output|read|dev-ready|judge|interrupt|cancel|queues|queue|move", output: "json", usage: [ "bun scripts/cli.ts codex deploy # disabled legacy deployment entry", "bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue id] [--dry-run]", "bun scripts/cli.ts codex task [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", - "bun scripts/cli.ts codex tasks [--queue id] [--limit N] [--unread-only]", + "bun scripts/cli.ts codex tasks [--view supervisor|full] [--queue id] [--status succeeded,running] [--unread|--unread-only] [--limit N] [--before-id id]", "bun scripts/cli.ts codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", + "bun scripts/cli.ts codex read ", + "bun scripts/cli.ts codex dev-ready", "bun scripts/cli.ts codex judge --attempt N [--dry-run] [--include-prompt]", "bun scripts/cli.ts codex interrupt|cancel ", - "bun scripts/cli.ts codex queues | queue create | queue merge --into | move --queue ", + "bun scripts/cli.ts codex queues [--full|--all] | queue create | queue merge --into | move --queue ", ], description: "Operate Code Queue through the stable backend-core private proxy path.", }; diff --git a/src/components/microservices/code-queue/Dockerfile b/src/components/microservices/code-queue/Dockerfile index 6f3b84ea..9eb7629e 100644 --- a/src/components/microservices/code-queue/Dockerfile +++ b/src/components/microservices/code-queue/Dockerfile @@ -2,6 +2,7 @@ ARG CODE_QUEUE_BASE_IMAGE=oven/bun:1-debian FROM ${CODE_QUEUE_BASE_IMAGE} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +ENV UNIDESK_SKILLS_PATH=/root/.agents/skills RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1) \ || (apt-get update \ @@ -57,6 +58,7 @@ COPY src/components/shared /app/src/components/shared WORKDIR /app/src/components/microservices/code-queue COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json COPY src/components/microservices/code-queue/src ./src +RUN mkdir -p /root/.agents/skills EXPOSE 4222 ENTRYPOINT ["tini", "--"] diff --git a/src/components/microservices/code-queue/docker-compose.d601.yml b/src/components/microservices/code-queue/docker-compose.d601.yml index 8064158b..21c31274 100644 --- a/src/components/microservices/code-queue/docker-compose.d601.yml +++ b/src/components/microservices/code-queue/docker-compose.d601.yml @@ -23,6 +23,7 @@ services: CODE_QUEUE_SCHEDULER_ENABLED: "${CODE_QUEUE_SCHEDULER_ENABLED:-true}" CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED: "${CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED:-false}" CODE_QUEUE_WORKDIR: "${CODE_QUEUE_WORKDIR:-/workspace}" + UNIDESK_SKILLS_PATH: "${UNIDESK_SKILLS_PATH:-/root/.agents/skills}" CODE_QUEUE_CODEX_HOME: "/var/lib/unidesk/code-queue/codex-home" CODE_QUEUE_OPENCODE_XDG_DIR: "/var/lib/unidesk/code-queue/opencode-xdg" CODE_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml" @@ -81,6 +82,7 @@ services: - ${CODE_QUEUE_WORKSPACE_PATH:-/home/ubuntu}:/workspace - ${CODE_QUEUE_CODEX_CONFIG_PATH:-/home/ubuntu/.codex/config.toml}:/root/.codex/config.toml:ro - ${CODE_QUEUE_CODEX_AUTH_PATH:-/home/ubuntu/.codex/auth.json}:/root/.codex/auth.json:ro + - ${CODE_QUEUE_SKILLS_PATH:-/home/ubuntu/.agents/skills}:/root/.agents/skills:ro - ${CODE_QUEUE_SSH_DIR:-/home/ubuntu/.ssh}:/root/.ssh:ro - ${CODE_QUEUE_LOG_DIR:-../../../../.state/code-queue/logs}:/var/log/unidesk - ${CODE_QUEUE_STATE_DIR:-../../../../.state/code-queue}:/var/lib/unidesk/code-queue diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index 62d16975..4bf05a1a 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -384,6 +384,7 @@ function readConfig(): RuntimeConfig { remoteDefaultWorkdir, executionProviderIds, remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL"]), + skillsPath: envString("UNIDESK_SKILLS_PATH", "/root/.agents/skills"), codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"), opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")), sourceCodexConfig: envString("CODE_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"), @@ -2413,6 +2414,74 @@ function runProbe(command: string, args: string[], timeout = 3_000): { ok: boole return { ok: result.status === 0, output }; } +function decodeMountInfoPath(value: string): string { + return value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8))); +} + +function mountInfoForPath(path: string): { mountPoint: string | null; readonly: boolean | null } { + try { + const target = resolve(path); + let best: { mountPoint: string; readonly: boolean } | null = null; + for (const line of readFileSync("/proc/self/mountinfo", "utf8").split(/\r?\n/u)) { + if (line.trim().length === 0) continue; + const fields = line.split(" "); + const mountPoint = decodeMountInfoPath(fields[4] ?? ""); + const options = (fields[5] ?? "").split(","); + if (mountPoint.length === 0) continue; + const matches = target === mountPoint || target.startsWith(mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`); + if (!matches) continue; + if (best === null || mountPoint.length > best.mountPoint.length) best = { mountPoint, readonly: options.includes("ro") }; + } + return best ?? { mountPoint: null, readonly: null }; + } catch { + return { mountPoint: null, readonly: null }; + } +} + +function collectSkillsStatus(): JsonValue { + const path = config.skillsPath; + const exists = existsSync(path); + const mountInfo = mountInfoForPath(path); + let directory = false; + let skillCount = 0; + let cliSpecAvailable = false; + let readonly = mountInfo.readonly === true; + let error: string | null = null; + if (exists) { + try { + const stat = statSync(path); + directory = stat.isDirectory(); + if (directory) { + const entries = readdirSync(path, { withFileTypes: true }); + skillCount = entries.filter((entry) => entry.isDirectory()).length; + cliSpecAvailable = existsSync(resolve(path, "cli-spec", "SKILL.md")); + if (mountInfo.readonly === null) { + const writeProbe = runProbe("sh", ["-lc", `test ! -w ${shellQuote(path)}`], 2_000); + readonly = writeProbe.ok; + } + } + } catch (probeError) { + error = probeError instanceof Error ? probeError.message : String(probeError); + } + } + const available = exists && directory; + return { + path, + mountPoint: mountInfo.mountPoint, + exists, + directory, + available, + readonly, + skillCount, + cliSpecAvailable, + expectedMount: "host ~/.agents/skills mounted read-only to UNIDESK_SKILLS_PATH", + repairHint: available && readonly && cliSpecAvailable + ? null + : "DEV code-queue should mount /home/ubuntu/.agents/skills read-only at /root/.agents/skills and set UNIDESK_SKILLS_PATH=/root/.agents/skills.", + error, + } as unknown as JsonValue; +} + function collectDevReady(): JsonValue { const now = Date.now(); if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value; @@ -2457,7 +2526,9 @@ function collectDevReady(): JsonValue { const sshKeyProbe = runProbe("sh", ["-lc", "test -d /root/.ssh && find /root/.ssh -maxdepth 1 -type f \\( -name 'id_*' ! -name '*.pub' \\) -perm -400 -print -quit"]); const githubKnownHostProbe = runProbe("ssh-keygen", ["-F", "github.com", "-f", "/root/.ssh/known_hosts"]); const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0; - const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady; + const skills = collectSkillsStatus() as Record; + const skillsReady = skills.available === true && skills.readonly === true && skills.cliSpecAvailable === true; + const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady; const value: JsonValue = { ok, missingTools, @@ -2489,6 +2560,7 @@ function collectDevReady(): JsonValue { githubKnownHostPresent: githubKnownHostProbe.ok, ready: sshSharedReady, }, + skills, }; devReadyCache = { checkedAtMs: now, value }; return value; @@ -5174,6 +5246,7 @@ async function route(req: Request): Promise { status: "starting", databaseReady, databaseLastError, + skills: collectSkillsStatus(), startedAt: serviceStartedAt, }, 503); return jsonResponse({ @@ -5190,6 +5263,7 @@ async function route(req: Request): Promise { schedulerPollIntervalMs: config.schedulerPollIntervalMs, queue: queueSummary(false, state.tasks), executionDiagnostics: executionDiagnosticsForTasks(state.tasks), + skills: collectSkillsStatus(), egressProxy: await providerGatewayEgressProxyStatus(), oaEventPublisher: oaEventPublisherStatus(), startedAt: serviceStartedAt, diff --git a/src/components/microservices/code-queue/src/prompts.ts b/src/components/microservices/code-queue/src/prompts.ts index 390c28d4..eb3aaa03 100644 --- a/src/components/microservices/code-queue/src/prompts.ts +++ b/src/components/microservices/code-queue/src/prompts.ts @@ -8,6 +8,7 @@ export const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示"; export const codeQueueEnvironmentHint = [ codeQueueEnvironmentHintTitle, "如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。", + "任务可通过 `UNIDESK_SKILLS_PATH`(默认 `/root/.agents/skills`)读取注入的宿主 skills;若需要确认注入状态,查询 Code Queue `/api/dev-ready` 的 `devReady.skills`,缺失时报告该字段的修复建议,不要读取或输出宿主 token/auth 配置。", ].join("\n"); export function stripAutoReferenceHint(prompt: string): string { diff --git a/src/components/microservices/code-queue/src/types.ts b/src/components/microservices/code-queue/src/types.ts index e5f303ab..ab6f041d 100644 --- a/src/components/microservices/code-queue/src/types.ts +++ b/src/components/microservices/code-queue/src/types.ts @@ -123,6 +123,7 @@ export interface RuntimeConfig { remoteDefaultWorkdir: string; executionProviderIds: string[]; remoteCodexEnvKeys: string[]; + skillsPath: string; codexHome: string; opencodeXdgDir: string; sourceCodexConfig: string; diff --git a/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml index eef072bb..9689de1b 100644 --- a/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml @@ -329,6 +329,8 @@ spec: value: danger-full-access - name: CODE_QUEUE_APPROVAL_POLICY value: never + - name: UNIDESK_SKILLS_PATH + value: /root/.agents/skills - name: CODE_QUEUE_EGRESS_PROXY_ENABLED value: "true" - name: CODE_QUEUE_EGRESS_PROXY_URL @@ -377,6 +379,9 @@ spec: - name: codex-auth mountPath: /root/.codex/auth.json readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true - name: ssh-dir mountPath: /root/.ssh readOnly: true @@ -433,6 +438,10 @@ spec: hostPath: path: /home/ubuntu/.codex/auth.json type: File + - name: skills-dir + hostPath: + path: /home/ubuntu/.agents/skills + type: Directory - name: ssh-dir hostPath: path: /home/ubuntu/.ssh @@ -531,6 +540,8 @@ spec: value: "2" - name: CODE_QUEUE_EGRESS_PROXY_ENABLED value: "false" + - name: UNIDESK_SKILLS_PATH + value: /root/.agents/skills - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED value: "false" - name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED @@ -543,6 +554,9 @@ spec: - name: repo mountPath: /root/unidesk readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true - name: logs mountPath: /var/log/unidesk-dev - name: state @@ -580,6 +594,10 @@ spec: hostPath: path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue type: Directory + - name: skills-dir + hostPath: + path: /home/ubuntu/.agents/skills + type: Directory - name: logs hostPath: path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs @@ -674,6 +692,8 @@ spec: value: "2" - name: CODE_QUEUE_EGRESS_PROXY_ENABLED value: "false" + - name: UNIDESK_SKILLS_PATH + value: /root/.agents/skills - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED value: "false" - name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED @@ -686,6 +706,9 @@ spec: - name: repo mountPath: /root/unidesk readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true - name: logs mountPath: /var/log/unidesk-dev - name: state @@ -723,6 +746,10 @@ spec: hostPath: path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue type: Directory + - name: skills-dir + hostPath: + path: /home/ubuntu/.agents/skills + type: Directory - name: logs hostPath: path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs