fix: add code queue pr preflight
This commit is contained in:
@@ -46,7 +46,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
|||||||
- `bun scripts/cli.ts gh auth status|issue ...|pr list|view|create|comment` / `bun scripts/code-queue-pr-preflight-example.ts`:通过 REST 执行安全 GitHub issue 读写、脱敏 auth/status 诊断、body-file Markdown 写入、#24 指挥简报新增时间线 ClaudeQQ 通知、escape 扫描与只读 cleanup-plan、PR 创建/评论 dry-run 和 runner PR preflight;`gh pr merge` 当前仍结构化拒绝,规则见 `docs/reference/cli.md` 和 `docs/reference/code-queue-supervision.md`。
|
- `bun scripts/cli.ts gh auth status|issue ...|pr list|view|create|comment` / `bun scripts/code-queue-pr-preflight-example.ts`:通过 REST 执行安全 GitHub issue 读写、脱敏 auth/status 诊断、body-file Markdown 写入、#24 指挥简报新增时间线 ClaudeQQ 通知、escape 扫描与只读 cleanup-plan、PR 创建/评论 dry-run 和 runner PR preflight;`gh pr merge` 当前仍结构化拒绝,规则见 `docs/reference/cli.md` 和 `docs/reference/code-queue-supervision.md`。
|
||||||
- `bun scripts/cli.ts ci install/status/run/publish-backend-core/publish-user-service/run-dev-e2e/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,支持每 commit 检查、Code Queue 只读性能门禁、`CI.json` catalog 驱动的 backend-core 与 user-service commit-pinned 镜像发布和手动触发的 `origin/master:deploy.json#environments.dev` 临时 namespace e2e;catalog/producer/consumer 分工见 `docs/reference/cicd-standardization.md`,`run-dev-e2e` 的 Git 控制 runner、短 launcher 和 no-CD 边界见 `docs/reference/dev-ci-runner.md`,Tekton 规则见 `docs/reference/ci.md`。
|
- `bun scripts/cli.ts ci install/status/run/publish-backend-core/publish-user-service/run-dev-e2e/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,支持每 commit 检查、Code Queue 只读性能门禁、`CI.json` catalog 驱动的 backend-core 与 user-service commit-pinned 镜像发布和手动触发的 `origin/master:deploy.json#environments.dev` 临时 namespace e2e;catalog/producer/consumer 分工见 `docs/reference/cicd-standardization.md`,`run-dev-e2e` 的 Git 控制 runner、短 launcher 和 no-CD 边界见 `docs/reference/dev-ci-runner.md`,Tekton 规则见 `docs/reference/ci.md`。
|
||||||
- `bun scripts/cli.ts codex deploy <commitId>`:旧 Code Queue 兼容部署入口已禁用,原因是它会绕过受控部署边界直连 D601 部署 Code Queue;规则见 `docs/reference/codex-deploy.md`。
|
- `bun scripts/cli.ts codex deploy <commitId>`:旧 Code Queue 兼容部署入口已禁用,原因是它会绕过受控部署边界直连 D601 部署 Code Queue;规则见 `docs/reference/codex-deploy.md`。
|
||||||
- `bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue <id>]`:通过 backend-core 私有代理提交 Code Queue 任务;控制面默认走主 server `code-queue-mgr` 写入 PostgreSQL,`--dry-run` 可只检查请求体不入队,规则见 `docs/reference/cli.md`。
|
- `bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue <id>]` / `codex pr-preflight [--remote]`:前者通过 backend-core 私有代理提交 Code Queue 任务;后者只读检查 D601 scheduler/runner 的 GitHub token、egress 和 PR 能力,PR 型派单前必须使用,规则见 `docs/reference/cli.md`。
|
||||||
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询默认审阅摘要,只返回原始 prompt、最终 response、最后错误和渐进披露命令;需要工具调用、attempt/judge 和详细耗时时显式加 `--detail`。
|
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询默认审阅摘要,只返回原始 prompt、最终 response、最后错误和渐进披露命令;需要工具调用、attempt/judge 和详细耗时时显式加 `--detail`。
|
||||||
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
||||||
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]`:通过 Code Queue 私有代理向运行中的 active turn 注入纠偏提示,正式替代底层 `microservice proxy ... /steer` 调用。
|
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]`:通过 Code Queue 私有代理向运行中的 active turn 注入纠偏提示,正式替代底层 `microservice proxy ... /steer` 调用。
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
|
|||||||
- `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs <scheduleId> --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs <scheduleId> --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run <id> --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run <failedRunId>` 只接受 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` 当成成功的空历史。
|
- `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs <scheduleId> --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs <scheduleId> --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run <id> --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run <failedRunId>` 只接受 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 <commitId>` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。
|
- `codex deploy <commitId>` 是旧 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 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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--issue N] [--full]` 通过稳定 `code-queue` proxy 请求 D601 scheduler `/api/runtime-preflight`,用于 PR 型派单 admission。输出会压缩展示 scheduler/runner 的 token 覆盖、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测和可选 push dry-run;只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。缺少 env token 时顶层 `ok=false`、`runnerDisposition=infra-blocked`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`。
|
||||||
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。需要旧式详细摘要时显式加 `--detail`;需要完整 prompt/response 文本时加 `--full`;需要工具调用、judge、attempt 全量摘要时使用 `--detail --full --tool-limit N`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。
|
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。需要旧式详细摘要时显式加 `--detail`;需要完整 prompt/response 文本时加 `--full`;需要工具调用、judge、attempt 全量摘要时使用 `--detail --full --tool-limit N`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。
|
||||||
- `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 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 <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,且仍以分页 trace 为主;需要完整 prompt/最终 response 时加 `--full`,需要详细 task 摘要时加 `--detail`。
|
- `codex task <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,且仍以分页 trace 为主;需要完整 prompt/最终 response 时加 `--full`,需要详细 task 摘要时加 `--detail`。
|
||||||
|
|||||||
@@ -105,13 +105,21 @@ Git/PR 交付要求:
|
|||||||
- final response 必须报告 head branch、PR URL、远端 head commit、修改文件、验证命令、是否未 merge。
|
- final response 必须报告 head branch、PR URL、远端 head commit、修改文件、验证命令、是否未 merge。
|
||||||
```
|
```
|
||||||
|
|
||||||
Runner preflight 示例:
|
Runner preflight 优先使用执行面诊断入口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun scripts/cli.ts codex pr-preflight --remote --issue 20
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令经 backend-core 稳定 `code-queue` proxy 访问 D601 scheduler 的 `/api/runtime-preflight`,报告 scheduler/runner 环境里的 `GH_TOKEN`/`GITHUB_TOKEN` 覆盖、工具、Git worktree、GitHub egress、repo/issue/PR 只读探测和可选 push dry-run。缺少 env token 时必须返回 `ok=false`、`runnerDisposition=infra-blocked` 和 `tokenCoverage.missing=["GH_TOKEN","GITHUB_TOKEN"]`,因为 provider dev container 只能转发 scheduler 已经拥有的 token。
|
||||||
|
|
||||||
|
本地 runner preflight 示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base master --head code-queue/issue-35-pr-dry-run-probe --comment-pr 1
|
bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base master --head code-queue/issue-35-pr-dry-run-probe --comment-pr 1
|
||||||
```
|
```
|
||||||
|
|
||||||
该脚本只读调用 `gh auth status`,并执行 `gh pr create --dry-run` 与 `gh pr comment --dry-run`。它检查 `GH_TOKEN/GITHUB_TOKEN` 是否存在、GitHub REST egress 是否可达、repo 是否可见,并且只输出 token 来源和存在性,不输出 token 值。`--comment-pr` 只是 dry-run 计划中的 PR number,不会写评论。
|
该脚本只读调用 `gh auth status`,并执行 `gh pr create --dry-run` 与 `gh pr comment --dry-run`。它检查当前 shell 的 `GH_TOKEN/GITHUB_TOKEN` 是否存在、GitHub REST egress 是否可达、repo 是否可见,并且只输出 token 来源和存在性,不输出 token 值。它不能证明 Code Queue default scheduler 已注入 token;跨 queue 派单 admission 应使用 `codex pr-preflight`。
|
||||||
|
|
||||||
指挥官审查 checklist:
|
指挥官审查 checklist:
|
||||||
|
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ D601 原生 k3s 的人工诊断必须显式使用 host kubeconfig:`KUBECONFIG=
|
|||||||
|
|
||||||
Code Queue worker 可以在任务明确要求审查型交付时创建 Pull Request。PR 交付不是默认出口;默认集成仍遵循项目当前 master-only 规则,直到具体任务或指挥官要求改为 PR。PR 型任务必须从最新目标线创建短生命周期分支,报告源分支、目标分支、PR URL、关联 issue、验证证据和未完成风险;分支命名应使用 `probe/`、`code-queue/` 或其他明确任务前缀,禁止把隐藏分支当成长期交付状态。
|
Code Queue worker 可以在任务明确要求审查型交付时创建 Pull Request。PR 交付不是默认出口;默认集成仍遵循项目当前 master-only 规则,直到具体任务或指挥官要求改为 PR。PR 型任务必须从最新目标线创建短生命周期分支,报告源分支、目标分支、PR URL、关联 issue、验证证据和未完成风险;分支命名应使用 `probe/`、`code-queue/` 或其他明确任务前缀,禁止把隐藏分支当成长期交付状态。
|
||||||
|
|
||||||
Code Queue runtime 提供 `/api/runtime-preflight` 作为 PR 能力探测入口。默认请求只检查本地工具、凭证可见性、Git worktree、HOME、known_hosts、agent port 和 proxy DNS;`/api/runtime-preflight?remote=1` 会增加 GitHub 网络、issue API、SSH/HTTPS `git ls-remote`、GitHub SSH、`gh auth status`、`gh repo view`、`gh issue view` 和只读 `gh pr list` 探测;`/api/runtime-preflight?remote=1&pushDryRun=1&pushDryRunRef=refs/heads/probe/<name>` 会额外执行 `git push --dry-run`,验证远端写权限但不创建分支。`issue=<number>` 可覆盖默认 issue #20 探针。探测输出只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在,不得输出 token 内容。
|
Code Queue runtime 提供 `/api/runtime-preflight` 作为 PR 能力探测入口;CLI 稳定入口是 `bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--issue N]`。默认请求只检查本地工具、凭证可见性、Git worktree、HOME、known_hosts、agent port 和 proxy DNS;`--remote` 会增加 GitHub 网络、issue API、SSH/HTTPS `git ls-remote`、GitHub SSH、`gh auth status`、`gh repo view`、`gh issue view` 和只读 `gh pr list` 探测;`--push-dry-run` 会额外执行 `git push --dry-run`,验证远端写权限但不创建分支。探测输出只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在,不得输出 token 内容。backend-core 的稳定 `code-queue` proxy 必须把 `/api/runtime-preflight` 路由到 D601 scheduler,而不是主 server `code-queue-mgr`,因为 token 和 PR runner 能力属于执行面环境。
|
||||||
|
|
||||||
PR 创建依赖以下最小运行时能力:
|
PR 创建依赖以下最小运行时能力:
|
||||||
|
|
||||||
- runtime image 必须包含 `git`、`gh`、`jq`、`ca-certificates`、`curl` 和 `openssh-client`;D601 provider dev container 准备脚本也必须补齐这些工具。
|
- runtime image 必须包含 `git`、`gh`、`jq`、`ca-certificates`、`curl` 和 `openssh-client`;D601 provider dev container 准备脚本也必须补齐这些工具。
|
||||||
- GitHub 凭证只能通过运行时 secret、环境变量或已有 SSH identity 注入。优先使用 `GH_TOKEN`,兼容 `GITHUB_TOKEN`;不得把 token 写入 Git remote、任务 prompt、日志、镜像或仓库文件。
|
- GitHub 凭证只能通过运行时 secret、环境变量或已有 SSH identity 注入。优先使用 `GH_TOKEN`,兼容 `GITHUB_TOKEN`;不得把 token 写入 Git remote、任务 prompt、日志、镜像或仓库文件。
|
||||||
- `CODE_QUEUE_REMOTE_CODEX_ENV_KEYS` 默认允许把 `GH_TOKEN`/`GITHUB_TOKEN` 以及 `GH_HOST`、`GITHUB_API_URL`、`GH_REPO` 从 scheduler 传给 provider dev container,供隔离执行环境内的 `gh repo view`、`gh issue view`、`gh pr list` 和 `gh pr create` 使用。
|
- `CODE_QUEUE_REMOTE_CODEX_ENV_KEYS` 默认允许把 `GH_TOKEN`/`GITHUB_TOKEN` 以及 `GH_HOST`、`GITHUB_API_URL`、`GH_REPO` 从 scheduler 传给 provider dev container,供隔离执行环境内的 `gh repo view`、`gh issue view`、`gh pr list` 和 `gh pr create` 使用。
|
||||||
- DEV 的持久注入路径复用 `unidesk-dev-runtime-secrets`:把 `GH_TOKEN` 或 `GITHUB_TOKEN` 写入该 Secret 后,通过受控 `deploy apply --env dev --service code-queue` 或等价 dev-only rollout 重启 Code Queue scheduler/read/write。不要在 PROD Code Queue 上直接 patch Secret 或 rollout。
|
- DEV 的持久注入路径复用 `unidesk-dev-runtime-secrets`:把 `GH_TOKEN` 或 `GITHUB_TOKEN` 写入该 Secret 后,通过受控 `deploy apply --env dev --service code-queue` 或等价 dev-only rollout 重启 Code Queue scheduler/read/write。default/prod runner 使用生产 `unidesk` namespace 的 `code-queue-env` Secret;仅修复 dev Secret 不会覆盖 default queue scheduler。不要在 PROD Code Queue 上直接 patch Secret 或 rollout,生产 token 覆盖需要显式 release/runtime 授权。
|
||||||
|
|
||||||
安全验证顺序固定为先只读、再 dry-run、最后才创建真实 PR。优先执行 `/api/runtime-preflight`、`/api/runtime-preflight?remote=1` 和带 `pushDryRun=1` 的 probe ref;只有工具、token、网络和 push dry-run 都满足且任务明确允许时,才创建 draft PR 或普通 PR。若创建真实 probe PR,最终报告必须记录 URL 并说明保留或关闭状态。
|
安全验证顺序固定为先只读、再 dry-run、最后才创建真实 PR。优先执行 `/api/runtime-preflight`、`/api/runtime-preflight?remote=1` 和带 `pushDryRun=1` 的 probe ref;只有工具、token、网络和 push dry-run 都满足且任务明确允许时,才创建 draft PR 或普通 PR。若创建真实 probe PR,最终报告必须记录 URL 并说明保留或关闭状态。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { codexPrPreflightQueryForTest } from "./src/code-queue";
|
||||||
|
|
||||||
|
type JsonRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||||
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): JsonRecord {
|
||||||
|
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), "expected JSON object", { value });
|
||||||
|
return value as JsonRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixtureRuntimePreflight(tokenPresent: boolean): JsonRecord {
|
||||||
|
return {
|
||||||
|
ok: tokenPresent,
|
||||||
|
checkedAt: "2026-05-20T00:00:00.000Z",
|
||||||
|
cwd: "/workspace/unidesk",
|
||||||
|
pid: 123,
|
||||||
|
ports: {
|
||||||
|
codex: { ok: true, commandPath: "/usr/local/bin/codex", version: "codex 0.128.0", errors: [] },
|
||||||
|
opencode: { ok: true, commandPath: "/usr/local/bin/opencode", version: "opencode 1.14.48", errors: [] },
|
||||||
|
},
|
||||||
|
pullRequestDelivery: {
|
||||||
|
ok: tokenPresent,
|
||||||
|
checkedAt: "2026-05-20T00:00:00.000Z",
|
||||||
|
tools: {
|
||||||
|
git: { ok: true, path: "/usr/bin/git", version: "git version 2.43.0" },
|
||||||
|
gh: { ok: true, path: "/usr/bin/gh", version: "gh version 2.45.0" },
|
||||||
|
hub: { ok: false, path: null, version: null },
|
||||||
|
jq: { ok: true, path: "/usr/bin/jq", version: "jq-1.7" },
|
||||||
|
ssh: { ok: true, path: "/usr/bin/ssh", version: "OpenSSH_9.6" },
|
||||||
|
curl: { ok: true, path: "/usr/bin/curl", version: "curl 8.5.0" },
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
ghTokenPresent: tokenPresent,
|
||||||
|
githubTokenPresent: false,
|
||||||
|
ghHostPresent: false,
|
||||||
|
githubApiUrlPresent: false,
|
||||||
|
ghRepoPresent: false,
|
||||||
|
sshAuthSockPresent: false,
|
||||||
|
gitAskpassPresent: false,
|
||||||
|
ghHostsConfigPresent: false,
|
||||||
|
gitCredentialsPresent: false,
|
||||||
|
},
|
||||||
|
githubContext: {
|
||||||
|
host: "github.com",
|
||||||
|
apiBaseUrl: "https://api.github.com",
|
||||||
|
repo: "pikasTech/unidesk",
|
||||||
|
issueProbeNumber: 20,
|
||||||
|
},
|
||||||
|
egress: {
|
||||||
|
proxy: { selectedProxyHost: "d601-provider-egress-proxy.unidesk.svc.cluster.local", selectedProxyPort: "18789", selectedProxyHostResolvable: true },
|
||||||
|
githubDefault: { command: "preflight", args: ["github-default-network"], ok: true, exitCode: 0, signal: null, error: null, stdout: "skipped", stderr: "" },
|
||||||
|
apiDefault: { command: "preflight", args: ["github-api-default-network"], ok: true, exitCode: 0, signal: null, error: null, stdout: "skipped", stderr: "" },
|
||||||
|
issueApi: null,
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
insideWorktree: true,
|
||||||
|
branch: "master",
|
||||||
|
head: "abc1234",
|
||||||
|
originMaster: "abc1234",
|
||||||
|
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
|
||||||
|
home: "/root",
|
||||||
|
homeWritable: true,
|
||||||
|
knownHostsPresent: true,
|
||||||
|
privateKeyPresent: true,
|
||||||
|
},
|
||||||
|
limitations: tokenPresent ? [] : ["GH_TOKEN/GITHUB_TOKEN is not present; gh cannot create PRs unless another gh credential store is mounted"],
|
||||||
|
risks: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixtureResponse(tokenPresent: boolean): JsonRecord {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
ok: true,
|
||||||
|
runtimePreflight: fixtureRuntimePreflight(tokenPresent),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runCodeQueuePrPreflightContract(): JsonRecord {
|
||||||
|
let observedPath = "";
|
||||||
|
const missing = codexPrPreflightQueryForTest(["--remote", "--issue", "35"], (path) => {
|
||||||
|
observedPath = path;
|
||||||
|
return fixtureResponse(false);
|
||||||
|
});
|
||||||
|
assertCondition(
|
||||||
|
observedPath === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&issue=35",
|
||||||
|
"PR preflight should route to the stable code-queue runtime preflight path",
|
||||||
|
{ observedPath },
|
||||||
|
);
|
||||||
|
assertCondition(asRecord(missing).ok === false, "missing token preflight should set top-level ok=false", missing);
|
||||||
|
const missingPreflight = asRecord(asRecord(missing).preflight);
|
||||||
|
assertCondition(missingPreflight.ok === false, "missing token preflight should fail", missingPreflight);
|
||||||
|
assertCondition(missingPreflight.runnerDisposition === "infra-blocked", "missing token must be infra-blocked", missingPreflight);
|
||||||
|
const missingTokenCoverage = asRecord(missingPreflight.tokenCoverage);
|
||||||
|
assertCondition(missingTokenCoverage.ok === false, "tokenCoverage should fail when no env token is present", missingTokenCoverage);
|
||||||
|
assertCondition(Array.isArray(missingTokenCoverage.missing) && missingTokenCoverage.missing.includes("GH_TOKEN") && missingTokenCoverage.missing.includes("GITHUB_TOKEN"), "tokenCoverage should name both accepted env keys", missingTokenCoverage);
|
||||||
|
assertCondition(!JSON.stringify(missing).includes("contract-token"), "preflight output must not leak token values", missing);
|
||||||
|
|
||||||
|
const ready = codexPrPreflightQueryForTest(["--remote", "--push-dry-run", "--push-dry-run-ref", "refs/heads/probe/test"], (path) => {
|
||||||
|
assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&pushDryRun=1&pushDryRunRef=refs%2Fheads%2Fprobe%2Ftest", "push dry-run options should map to query string", { path });
|
||||||
|
return fixtureResponse(true);
|
||||||
|
});
|
||||||
|
const readyPreflight = asRecord(asRecord(ready).preflight);
|
||||||
|
assertCondition(asRecord(ready).ok === true, "token-ready preflight should set top-level ok=true", ready);
|
||||||
|
assertCondition(readyPreflight.ok === true, "token-ready preflight should pass fixture", readyPreflight);
|
||||||
|
assertCondition(readyPreflight.runnerDisposition === "ready", "ready preflight should report ready disposition", readyPreflight);
|
||||||
|
const readyTokenCoverage = asRecord(readyPreflight.tokenCoverage);
|
||||||
|
assertCondition(readyTokenCoverage.source === "GH_TOKEN", "ready token source should be redacted to key name only", readyTokenCoverage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
checks: [
|
||||||
|
"stable runtime-preflight proxy path",
|
||||||
|
"missing GitHub token is infra-blocked",
|
||||||
|
"token key names are reported without values",
|
||||||
|
"push dry-run options are forwarded",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
process.stdout.write(`${JSON.stringify(runCodeQueuePrPreflightContract(), null, 2)}\n`);
|
||||||
|
}
|
||||||
@@ -281,6 +281,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
|||||||
fileItem("scripts/code-queue-liveness-diagnostics-test.ts"),
|
fileItem("scripts/code-queue-liveness-diagnostics-test.ts"),
|
||||||
fileItem("scripts/src/code-queue-liveness-fixtures.ts"),
|
fileItem("scripts/src/code-queue-liveness-fixtures.ts"),
|
||||||
fileItem("scripts/code-queue-trace-summary-contract-test.ts"),
|
fileItem("scripts/code-queue-trace-summary-contract-test.ts"),
|
||||||
|
fileItem("scripts/code-queue-pr-preflight-contract-test.ts"),
|
||||||
fileItem("scripts/src/ci.ts"),
|
fileItem("scripts/src/ci.ts"),
|
||||||
fileItem("scripts/src/e2e.ts"),
|
fileItem("scripts/src/e2e.ts"),
|
||||||
fileItem("scripts/code-queue-prompt-observation-test.ts"),
|
fileItem("scripts/code-queue-prompt-observation-test.ts"),
|
||||||
@@ -299,6 +300,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
|||||||
items.push(commandItem("code-queue:prompt-observation-contract", ["bun", "scripts/code-queue-prompt-observation-test.ts"], 30_000));
|
items.push(commandItem("code-queue:prompt-observation-contract", ["bun", "scripts/code-queue-prompt-observation-test.ts"], 30_000));
|
||||||
items.push(commandItem("code-queue:issue3-diagnostics-and-image-preflight", ["bun", "scripts/code-queue-issue3-regression-test.ts"], 30_000));
|
items.push(commandItem("code-queue:issue3-diagnostics-and-image-preflight", ["bun", "scripts/code-queue-issue3-regression-test.ts"], 30_000));
|
||||||
items.push(commandItem("code-queue:trace-summary-contract", ["bun", "scripts/code-queue-trace-summary-contract-test.ts"], 30_000));
|
items.push(commandItem("code-queue:trace-summary-contract", ["bun", "scripts/code-queue-trace-summary-contract-test.ts"], 30_000));
|
||||||
|
items.push(commandItem("code-queue:pr-preflight-contract", ["bun", "scripts/code-queue-pr-preflight-contract-test.ts"], 30_000));
|
||||||
items.push(commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000));
|
items.push(commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000));
|
||||||
items.push(commandItem("code-queue:trace-gap-not-stale", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:trace-gap-not-stale"], 30_000));
|
items.push(commandItem("code-queue:trace-gap-not-stale", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:trace-gap-not-stale"], 30_000));
|
||||||
items.push(commandItem("code-queue:stale-active-owner-expired", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:stale-active-owner-expired"], 30_000));
|
items.push(commandItem("code-queue:stale-active-owner-expired", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:stale-active-owner-expired"], 30_000));
|
||||||
@@ -313,6 +315,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
|||||||
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
items.push(skippedItem("code-queue:issue3-diagnostics-and-image-preflight", "Code Queue issue #3 regression fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("code-queue:issue3-diagnostics-and-image-preflight", "Code Queue issue #3 regression fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
|
items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
items.push(skippedItem("schedule:cli-contract", "Schedule CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
items.push(skippedItem("schedule:cli-contract", "Schedule CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||||
|
|||||||
+192
-1
@@ -147,6 +147,14 @@ interface CodexQueuesOptions {
|
|||||||
limitExplicit: boolean;
|
limitExplicit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CodexPrPreflightOptions {
|
||||||
|
remote: boolean;
|
||||||
|
pushDryRun: boolean;
|
||||||
|
pushDryRunRef: string | undefined;
|
||||||
|
issueNumber: number | null;
|
||||||
|
full: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type CodexRequestInit = { method?: string; body?: unknown };
|
type CodexRequestInit = { method?: string; body?: unknown };
|
||||||
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
||||||
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
||||||
@@ -994,6 +1002,20 @@ function parseQueuesOptions(args: string[]): CodexQueuesOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePrPreflightOptions(args: string[]): CodexPrPreflightOptions {
|
||||||
|
assertKnownOptions(args, {
|
||||||
|
flags: ["--remote", "--push-dry-run", "--pushDryRun", "--full", "--raw"],
|
||||||
|
valueOptions: ["--push-dry-run-ref", "--pushDryRunRef", "--issue", "--issue-number", "--issueNumber"],
|
||||||
|
}, "codex pr-preflight");
|
||||||
|
return {
|
||||||
|
remote: hasFlag(args, "--remote"),
|
||||||
|
pushDryRun: hasFlag(args, "--push-dry-run") || hasFlag(args, "--pushDryRun"),
|
||||||
|
pushDryRunRef: optionValue(args, ["--push-dry-run-ref", "--pushDryRunRef"]),
|
||||||
|
issueNumber: nullablePositiveNumberOption(args, ["--issue", "--issue-number", "--issueNumber"]),
|
||||||
|
full: hasFlag(args, "--full") || hasFlag(args, "--raw"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
|
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
|
||||||
assertKnownOptions(args, {
|
assertKnownOptions(args, {
|
||||||
flags: ["--dry-run", "--no-call", "--include-prompt"],
|
flags: ["--dry-run", "--no-call", "--include-prompt"],
|
||||||
@@ -1959,6 +1981,174 @@ function codeQueueDevReady(): unknown {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactCommandProbe(value: unknown): Record<string, unknown> | null {
|
||||||
|
const probe = asRecord(value);
|
||||||
|
if (probe === null) return null;
|
||||||
|
return {
|
||||||
|
command: probe.command ?? null,
|
||||||
|
args: Array.isArray(probe.args) ? probe.args : [],
|
||||||
|
ok: probe.ok ?? false,
|
||||||
|
exitCode: probe.exitCode ?? null,
|
||||||
|
signal: probe.signal ?? null,
|
||||||
|
error: probe.error ?? null,
|
||||||
|
stdout: typeof probe.stdout === "string" ? textView(probe.stdout, false, 1200) : null,
|
||||||
|
stderr: typeof probe.stderr === "string" ? textView(probe.stderr, false, 1200) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactToolStatus(value: unknown): Record<string, unknown> {
|
||||||
|
const tool = asRecord(value) ?? {};
|
||||||
|
return {
|
||||||
|
ok: tool.ok ?? false,
|
||||||
|
path: tool.path ?? null,
|
||||||
|
version: tool.version ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactAgentPortStatus(value: unknown): Record<string, unknown> {
|
||||||
|
const port = asRecord(value) ?? {};
|
||||||
|
return {
|
||||||
|
ok: port.ok ?? false,
|
||||||
|
commandPath: port.commandPath ?? null,
|
||||||
|
version: port.version ?? null,
|
||||||
|
errors: Array.isArray(port.errors) ? port.errors : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenCoverageStatus(credentials: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const ghTokenPresent = credentials.ghTokenPresent === true;
|
||||||
|
const githubTokenPresent = credentials.githubTokenPresent === true;
|
||||||
|
const anyEnvToken = ghTokenPresent || githubTokenPresent;
|
||||||
|
const anyGhCredentialStore = credentials.ghHostsConfigPresent === true || credentials.gitCredentialsPresent === true;
|
||||||
|
return {
|
||||||
|
ok: anyEnvToken,
|
||||||
|
source: ghTokenPresent ? "GH_TOKEN" : githubTokenPresent ? "GITHUB_TOKEN" : null,
|
||||||
|
ghTokenPresent,
|
||||||
|
githubTokenPresent,
|
||||||
|
ghCredentialStorePresent: anyGhCredentialStore,
|
||||||
|
runnerDisposition: anyEnvToken ? "ready" : "infra-blocked",
|
||||||
|
missing: anyEnvToken ? [] : ["GH_TOKEN", "GITHUB_TOKEN"],
|
||||||
|
scope: "scheduler-runner-env",
|
||||||
|
note: anyEnvToken
|
||||||
|
? "scheduler has a GitHub env token that can be forwarded to provider dev containers through CODE_QUEUE_REMOTE_CODEX_ENV_KEYS"
|
||||||
|
: "scheduler is missing GH_TOKEN/GITHUB_TOKEN; provider dev containers cannot receive a GitHub token even though CODE_QUEUE_REMOTE_CODEX_ENV_KEYS includes those keys",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> {
|
||||||
|
const pull = asRecord(preflight.pullRequestDelivery) ?? {};
|
||||||
|
const tools = asRecord(pull.tools) ?? {};
|
||||||
|
const credentials = asRecord(pull.credentials) ?? {};
|
||||||
|
const git = asRecord(pull.git) ?? {};
|
||||||
|
const githubContext = asRecord(pull.githubContext) ?? {};
|
||||||
|
const egress = asRecord(pull.egress) ?? {};
|
||||||
|
const proxy = asRecord(egress.proxy) ?? {};
|
||||||
|
const remote = asRecord(pull.remote);
|
||||||
|
const ports = asRecord(preflight.ports) ?? {};
|
||||||
|
const tokenCoverage = tokenCoverageStatus(credentials);
|
||||||
|
const limitations = Array.isArray(pull.limitations) ? pull.limitations.map(String) : [];
|
||||||
|
const risks = Array.isArray(pull.risks) ? pull.risks.map(String) : [];
|
||||||
|
const ok = preflight.ok === true && tokenCoverage.ok === true;
|
||||||
|
const result: Record<string, unknown> = {
|
||||||
|
ok,
|
||||||
|
checkedAt: preflight.checkedAt ?? pull.checkedAt ?? null,
|
||||||
|
runner: {
|
||||||
|
serviceId: "code-queue",
|
||||||
|
plane: "D601 k3s scheduler/runner",
|
||||||
|
queueScope: "all queues executed by the scheduler, including default",
|
||||||
|
cwd: preflight.cwd ?? null,
|
||||||
|
pid: preflight.pid ?? null,
|
||||||
|
},
|
||||||
|
tokenCoverage,
|
||||||
|
tools: {
|
||||||
|
git: compactToolStatus(tools.git),
|
||||||
|
gh: compactToolStatus(tools.gh),
|
||||||
|
hub: compactToolStatus(tools.hub),
|
||||||
|
jq: compactToolStatus(tools.jq),
|
||||||
|
ssh: compactToolStatus(tools.ssh),
|
||||||
|
curl: compactToolStatus(tools.curl),
|
||||||
|
},
|
||||||
|
agentPorts: {
|
||||||
|
codex: compactAgentPortStatus(ports.codex),
|
||||||
|
opencode: compactAgentPortStatus(ports.opencode),
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
insideWorktree: git.insideWorktree ?? false,
|
||||||
|
branch: git.branch ?? null,
|
||||||
|
head: git.head ?? null,
|
||||||
|
originMaster: git.originMaster ?? null,
|
||||||
|
remoteOrigin: git.remoteOrigin ?? null,
|
||||||
|
home: git.home ?? null,
|
||||||
|
homeWritable: git.homeWritable ?? false,
|
||||||
|
knownHostsPresent: git.knownHostsPresent ?? false,
|
||||||
|
privateKeyPresent: git.privateKeyPresent ?? false,
|
||||||
|
},
|
||||||
|
githubContext: {
|
||||||
|
host: githubContext.host ?? null,
|
||||||
|
apiBaseUrl: githubContext.apiBaseUrl ?? null,
|
||||||
|
repo: githubContext.repo ?? null,
|
||||||
|
issueProbeNumber: githubContext.issueProbeNumber ?? null,
|
||||||
|
},
|
||||||
|
egress: {
|
||||||
|
proxy: {
|
||||||
|
selectedProxyHost: proxy.selectedProxyHost ?? null,
|
||||||
|
selectedProxyPort: proxy.selectedProxyPort ?? null,
|
||||||
|
selectedProxyHostResolvable: proxy.selectedProxyHostResolvable ?? null,
|
||||||
|
},
|
||||||
|
githubDefault: compactCommandProbe(egress.githubDefault),
|
||||||
|
apiDefault: compactCommandProbe(egress.apiDefault),
|
||||||
|
issueApi: compactCommandProbe(egress.issueApi),
|
||||||
|
},
|
||||||
|
remote: remote === null ? null : {
|
||||||
|
gitLsRemote: compactCommandProbe(remote.gitLsRemote),
|
||||||
|
gitHttpsLsRemote: compactCommandProbe(remote.gitHttpsLsRemote),
|
||||||
|
githubSshAuthenticated: remote.githubSshAuthenticated ?? false,
|
||||||
|
ghAuthStatus: compactCommandProbe(remote.ghAuthStatus),
|
||||||
|
ghRepoView: compactCommandProbe(remote.ghRepoView),
|
||||||
|
ghIssueView: compactCommandProbe(remote.ghIssueView),
|
||||||
|
ghPrReadOnly: compactCommandProbe(remote.ghPrReadOnly),
|
||||||
|
},
|
||||||
|
pushDryRun: compactCommandProbe(pull.pushDryRun),
|
||||||
|
limitations,
|
||||||
|
risks,
|
||||||
|
runnerDisposition: ok ? "ready" : "infra-blocked",
|
||||||
|
recoveryHint: ok
|
||||||
|
? "Runner PR workflow has env-token coverage for the scheduler."
|
||||||
|
: "Inject GH_TOKEN or GITHUB_TOKEN into the Code Queue scheduler runtime secret for the target queue, then rerun this preflight before creating a PR.",
|
||||||
|
commands: {
|
||||||
|
local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk",
|
||||||
|
runner: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||||
|
runnerPushDryRun: "bun scripts/cli.ts codex pr-preflight --remote --push-dry-run --push-dry-run-ref refs/heads/probe/code-queue-pr-capability",
|
||||||
|
rawProxy: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight?remote=1 --raw",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (options.full) result.rawRuntimePreflight = preflight;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
|
||||||
|
const options = parsePrPreflightOptions(optionArgs);
|
||||||
|
const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({
|
||||||
|
remote: options.remote ? 1 : undefined,
|
||||||
|
pushDryRun: options.pushDryRun ? 1 : undefined,
|
||||||
|
pushDryRunRef: options.pushDryRunRef,
|
||||||
|
issue: options.issueNumber,
|
||||||
|
})}`);
|
||||||
|
const response = unwrapCodexResponse(fetcher(path));
|
||||||
|
const preflight = asRecord(response.body.runtimePreflight);
|
||||||
|
if (preflight === null) throw new Error("Code Queue runtime-preflight response did not include runtimePreflight");
|
||||||
|
const compact = compactPrRuntimePreflight(preflight, options);
|
||||||
|
return {
|
||||||
|
ok: compact.ok,
|
||||||
|
upstream: response.upstream,
|
||||||
|
preflight: compact,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function codexPrPreflightQueryForTest(optionArgs: string[], fetcher: CodexResponseFetcher): unknown {
|
||||||
|
return codeQueuePrPreflight(optionArgs, fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
function codexSubmitTask(args: string[]): unknown {
|
function codexSubmitTask(args: string[]): unknown {
|
||||||
const options = parseSubmitOptions(args);
|
const options = parseSubmitOptions(args);
|
||||||
const payload = submitPayload(options);
|
const payload = submitPayload(options);
|
||||||
@@ -2069,6 +2259,7 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
|||||||
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
|
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
|
||||||
return codeQueueDevReady();
|
return codeQueueDevReady();
|
||||||
}
|
}
|
||||||
|
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1));
|
||||||
if (action === "output") {
|
if (action === "output") {
|
||||||
const taskId = requireTaskId(taskIdArg, "codex output");
|
const taskId = requireTaskId(taskIdArg, "codex output");
|
||||||
return codexOutputQuery(taskId, args.slice(2));
|
return codexOutputQuery(taskId, args.slice(2));
|
||||||
@@ -2103,5 +2294,5 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
|||||||
const taskId = requireTaskId(taskIdArg, "codex steer");
|
const taskId = requireTaskId(taskIdArg, "codex steer");
|
||||||
return codexSteerTask(taskId, args.slice(2));
|
return codexSteerTask(taskId, args.slice(2));
|
||||||
}
|
}
|
||||||
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, steer, 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, pr-preflight, runtime-preflight, queues, queue list, queue create, queue merge, move, steer, interrupt, cancel");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -49,6 +49,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: "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 <commitId> [--provider-id D601] [--timeout-ms N]", description: "Disabled legacy Code Queue deploy path; use the dev-only artifact consumer instead." },
|
{ command: "codex deploy <commitId> [--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 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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--issue N]", description: "Read-only PR admission check against the D601 scheduler/runner token, GitHub egress, repo visibility, and optional push dry-run." },
|
||||||
{ command: "codex task <taskId> [--detail] [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch the bounded review view by default: original prompt, final response, and drill-down commands; detail and trace are opt-in." },
|
{ command: "codex task <taskId> [--detail] [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch the bounded review view by default: original prompt, final response, and drill-down commands; detail and trace are opt-in." },
|
||||||
{ 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 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 <taskId> [--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 output <taskId> [--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." },
|
||||||
@@ -204,7 +205,7 @@ function scheduleHelp(): unknown {
|
|||||||
|
|
||||||
function codexHelp(): unknown {
|
function codexHelp(): unknown {
|
||||||
return {
|
return {
|
||||||
command: "codex deploy|submit|task|tasks|output|read|dev-ready|judge|steer|interrupt|cancel|queues|queue|move",
|
command: "codex deploy|submit|task|tasks|output|read|dev-ready|pr-preflight|judge|steer|interrupt|cancel|queues|queue|move",
|
||||||
output: "json",
|
output: "json",
|
||||||
usage: [
|
usage: [
|
||||||
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
|
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
|
||||||
@@ -214,6 +215,7 @@ function codexHelp(): unknown {
|
|||||||
"bun scripts/cli.ts codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]",
|
"bun scripts/cli.ts codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]",
|
||||||
"bun scripts/cli.ts codex read <taskId>",
|
"bun scripts/cli.ts codex read <taskId>",
|
||||||
"bun scripts/cli.ts codex dev-ready",
|
"bun scripts/cli.ts codex dev-ready",
|
||||||
|
"bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--issue N]",
|
||||||
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
|
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
|
||||||
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]",
|
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]",
|
||||||
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
|
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ function isK3sctlManagedMicroservice(service: MicroserviceConfig): boolean {
|
|||||||
|
|
||||||
function codeQueueK3sServiceIdForRequest(method: string, targetPath: string): string {
|
function codeQueueK3sServiceIdForRequest(method: string, targetPath: string): string {
|
||||||
const normalizedMethod = method.toUpperCase();
|
const normalizedMethod = method.toUpperCase();
|
||||||
if (targetPath === "/" || targetPath === "/health" || targetPath === "/live" || targetPath === "/api/dev-ready") return "code-queue-scheduler";
|
if (targetPath === "/" || targetPath === "/health" || targetPath === "/live" || targetPath === "/api/dev-ready" || targetPath === "/api/runtime-preflight") return "code-queue-scheduler";
|
||||||
if (targetPath === "/api/queues" || targetPath === "/api/tasks/overview") return "code-queue-scheduler";
|
if (targetPath === "/api/queues" || targetPath === "/api/tasks/overview") return "code-queue-scheduler";
|
||||||
if (targetPath === "/api/oa/backfill" || targetPath === "/api/notifications/claudeqq/drain" || targetPath === "/api/notifications/claudeqq/backfill") return "code-queue-scheduler";
|
if (targetPath === "/api/oa/backfill" || targetPath === "/api/notifications/claudeqq/drain" || targetPath === "/api/notifications/claudeqq/backfill") return "code-queue-scheduler";
|
||||||
if (targetPath === "/api/judge/probe" || targetPath === "/api/judge/self-test" || targetPath === "/api/queue-order/self-test" || targetPath === "/api/reference-injection/self-test" || targetPath === "/api/trace-port/self-test") return "code-queue-scheduler";
|
if (targetPath === "/api/judge/probe" || targetPath === "/api/judge/self-test" || targetPath === "/api/queue-order/self-test" || targetPath === "/api/reference-injection/self-test" || targetPath === "/api/trace-port/self-test") return "code-queue-scheduler";
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ fn direct_code_queue_mgr_service(
|
|||||||
|
|
||||||
fn code_queue_k3s_service_id_for_request(method: &Method, target_path: &str) -> &'static str {
|
fn code_queue_k3s_service_id_for_request(method: &Method, target_path: &str) -> &'static str {
|
||||||
let method = method.as_str();
|
let method = method.as_str();
|
||||||
if matches!(target_path, "/" | "/health" | "/live" | "/api/dev-ready") {
|
if matches!(target_path, "/" | "/health" | "/live" | "/api/dev-ready" | "/api/runtime-preflight") {
|
||||||
return "code-queue-scheduler";
|
return "code-queue-scheduler";
|
||||||
}
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
|
|||||||
Reference in New Issue
Block a user