From 9e4561e1026cebe9b0f7c1cfa4a37e8c2bf2f8b9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 01:14:24 +0000 Subject: [PATCH] fix: extend code queue pr preflight diagnostics --- docs/reference/cli.md | 2 +- docs/reference/codex-deploy.md | 4 +- .../code-queue-pr-preflight-contract-test.ts | 10 +++ scripts/src/code-queue.ts | 12 +++- scripts/src/help.ts | 4 +- .../microservices/code-queue/src/index.ts | 2 + .../code-queue/src/runtime-preflight.ts | 61 ++++++++++++++++++- 7 files changed, 87 insertions(+), 8 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f18bd2f1..65c6c565 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -41,7 +41,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 --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run ` 只接受 failed run,从原 run 反查 `scheduleId` 后重触发同一 schedule,并输出 `originalRunId`、`scheduleId`、`newRunId` 和 `observeCommand`。当 backend-core 目标容器缺失或只观察到 verify-only 容器时,schedule/microservice 命令必须以非零退出并返回 `failureKind=target-stack-not-running`、`runnerDisposition=infra-blocked`、`readOnlyCommands` 和 `authorizationRequiredForRecovery`,不得把 Docker 的 `No such container` 当成成功的空历史。 - `codex deploy ` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 - `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 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/证据要求等 guard 状态;该建议只用于指挥官 preflight,不会改写 payload,也不假设生产 MiniMax 可用。提交确认和 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/] [--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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full]` 通过稳定 `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,以及可选 PR body/create dry-run guard;只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。缺少 env token 时顶层 `ok=false`、`runnerDisposition=infra-blocked`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token auth、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 - `codex task ` 通过 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 task --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`。 diff --git a/docs/reference/codex-deploy.md b/docs/reference/codex-deploy.md index 4cfab33c..b8518ce3 100644 --- a/docs/reference/codex-deploy.md +++ b/docs/reference/codex-deploy.md @@ -61,7 +61,7 @@ D601 原生 k3s 的人工诊断必须显式使用 host kubeconfig:`KUBECONFIG= Code Queue worker 可以在任务明确要求审查型交付时创建 Pull Request。PR 交付不是默认出口;默认集成仍遵循项目当前 master-only 规则,直到具体任务或指挥官要求改为 PR。PR 型任务必须从最新目标线创建短生命周期分支,报告源分支、目标分支、PR URL、关联 issue、验证证据和未完成风险;分支命名应使用 `probe/`、`code-queue/` 或其他明确任务前缀,禁止把隐藏分支当成长期交付状态。 -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/] [--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 能力属于执行面环境。 +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/] [--pr-create-dry-run --pr-create-dry-run-head ] [--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`,验证远端写权限但不创建分支;`--pr-create-dry-run` 会在 runner 内生成受控 PR body 并执行 `scripts/cli.ts gh pr create --dry-run`,只证明 PR body guard 和命令形态可用,不 POST GitHub。探测输出只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在,不得输出 token 内容。backend-core 的稳定 `code-queue` proxy 必须把 `/api/runtime-preflight` 路由到 D601 scheduler,而不是主 server `code-queue-mgr`,因为 token 和 PR runner 能力属于执行面环境。 PR 创建依赖以下最小运行时能力: @@ -70,7 +70,7 @@ PR 创建依赖以下最小运行时能力: - `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。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,以及带 `prCreateDryRun=1` 的 PR body/create dry-run guard;只有工具、token、网络、push dry-run 和 PR body guard 都满足且任务明确允许时,才创建 draft PR 或普通 PR。若创建真实 probe PR,最终报告必须记录 URL 并说明保留或关闭状态。 preflight failure 必须定位到具体缺失项。已知必须区分的状态包括:`gh`/`hub` 缺失、`GH_TOKEN`/`GITHUB_TOKEN`/`GH_HOST`/`GITHUB_API_URL`/`GH_REPO` 未注入、`/root/.config/gh/hosts.yml` 与 `/root/.git-credentials` 缺失、SSH Git 可读但 GitHub issue/API 匿名访问返回 404、匿名 HTTPS Git 不可用、以及 `HTTP_PROXY`/`HTTPS_PROXY` 指向的 egress proxy 在 runner 内不可解析。发现这类缺口时,worker 应报告 preflight error 和阻塞项,不得强行创建 PR。 diff --git a/scripts/code-queue-pr-preflight-contract-test.ts b/scripts/code-queue-pr-preflight-contract-test.ts index d917c4c5..bc8a7c64 100644 --- a/scripts/code-queue-pr-preflight-contract-test.ts +++ b/scripts/code-queue-pr-preflight-contract-test.ts @@ -66,6 +66,7 @@ function fixtureRuntimePreflight(tokenPresent: boolean): JsonRecord { knownHostsPresent: true, privateKeyPresent: true, }, + prCreateDryRun: tokenPresent ? { command: "sh", args: ["-lc", "bun scripts/cli.ts gh pr create --dry-run"], ok: true, exitCode: 0, signal: null, error: null, stdout: "{\"ok\":true,\"data\":{\"dryRun\":true}}", stderr: "" } : undefined, limitations: tokenPresent ? [] : ["GH_TOKEN/GITHUB_TOKEN is not present; gh cannot create PRs unless another gh credential store is mounted"], risks: [], }, @@ -114,6 +115,14 @@ export function runCodeQueuePrPreflightContract(): JsonRecord { const readyTokenCoverage = asRecord(readyPreflight.tokenCoverage); assertCondition(readyTokenCoverage.source === "GH_TOKEN", "ready token source should be redacted to key name only", readyTokenCoverage); + const prCreateDryRun = codexPrPreflightQueryForTest(["--remote", "--pr-create-dry-run", "--pr-create-dry-run-head", "codequeue/pr-probe"], (path) => { + assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&prCreateDryRun=1&prCreateDryRunHead=codequeue%2Fpr-probe", "PR create dry-run options should map to query string", { path }); + return fixtureResponse(true); + }); + const prCreateDryRunPreflight = asRecord(asRecord(prCreateDryRun).preflight); + const prCreateDryRunProbe = asRecord(prCreateDryRunPreflight.prCreateDryRun); + assertCondition(prCreateDryRunProbe.ok === true, "PR create dry-run probe should be compacted", prCreateDryRunProbe); + return { ok: true, checks: [ @@ -121,6 +130,7 @@ export function runCodeQueuePrPreflightContract(): JsonRecord { "missing GitHub token is infra-blocked", "token key names are reported without values", "push dry-run options are forwarded", + "PR create dry-run options are forwarded", ], }; } diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 0b80c345..29ab84c8 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -190,6 +190,8 @@ interface CodexPrPreflightOptions { remote: boolean; pushDryRun: boolean; pushDryRunRef: string | undefined; + prCreateDryRun: boolean; + prCreateDryRunHead: string | undefined; issueNumber: number | null; full: boolean; } @@ -1259,13 +1261,15 @@ 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"], + flags: ["--remote", "--push-dry-run", "--pushDryRun", "--pr-create-dry-run", "--prCreateDryRun", "--full", "--raw"], + valueOptions: ["--push-dry-run-ref", "--pushDryRunRef", "--pr-create-dry-run-head", "--prCreateDryRunHead", "--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"]), + prCreateDryRun: hasFlag(args, "--pr-create-dry-run") || hasFlag(args, "--prCreateDryRun"), + prCreateDryRunHead: optionValue(args, ["--pr-create-dry-run-head", "--prCreateDryRunHead"]), issueNumber: nullablePositiveNumberOption(args, ["--issue", "--issue-number", "--issueNumber"]), full: hasFlag(args, "--full") || hasFlag(args, "--raw"), }; @@ -2364,6 +2368,7 @@ function compactPrRuntimePreflight(preflight: Record, options: ghPrReadOnly: compactCommandProbe(remote.ghPrReadOnly), }, pushDryRun: compactCommandProbe(pull.pushDryRun), + prCreateDryRun: compactCommandProbe(pull.prCreateDryRun), limitations, risks, runnerDisposition: ok ? "ready" : "infra-blocked", @@ -2374,6 +2379,7 @@ function compactPrRuntimePreflight(preflight: Record, options: 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", + runnerPrCreateDryRun: "bun scripts/cli.ts codex pr-preflight --remote --pr-create-dry-run --pr-create-dry-run-head ", rawProxy: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight?remote=1 --raw", }, }; @@ -2387,6 +2393,8 @@ function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseF remote: options.remote ? 1 : undefined, pushDryRun: options.pushDryRun ? 1 : undefined, pushDryRunRef: options.pushDryRunRef, + prCreateDryRun: options.prCreateDryRun ? 1 : undefined, + prCreateDryRunHead: options.prCreateDryRunHead, issue: options.issueNumber, })}`); const response = unwrapCodexResponse(fetcher(path)); diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 69639c28..fd0607fb 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -49,7 +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: "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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N]", description: "Read-only PR admission check against the D601 scheduler/runner token, GitHub egress, repo visibility, optional push dry-run, and PR body/create dry-run guard." }, { command: "codex task [--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 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." }, @@ -215,7 +215,7 @@ function codexHelp(): unknown { "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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--issue N]", + "bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N]", "bun scripts/cli.ts codex judge --attempt N [--dry-run] [--include-prompt]", "bun scripts/cli.ts codex steer [prompt|--prompt-file path|--prompt-stdin] [--dry-run]", "bun scripts/cli.ts codex interrupt|cancel ", diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index 0de20934..07df2815 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -5284,6 +5284,8 @@ async function route(req: Request): Promise { includeRemote: url.searchParams.get("remote") === "1", includePushDryRun: url.searchParams.get("pushDryRun") === "1", pushDryRunRef: url.searchParams.get("pushDryRunRef") ?? undefined, + includePrCreateDryRun: url.searchParams.get("prCreateDryRun") === "1", + prCreateDryRunHead: url.searchParams.get("prCreateDryRunHead") ?? undefined, issueNumber: Number(url.searchParams.get("issue") ?? url.searchParams.get("issueNumber") ?? NaN), })), }); diff --git a/src/components/microservices/code-queue/src/runtime-preflight.ts b/src/components/microservices/code-queue/src/runtime-preflight.ts index cfcca1d2..7c2f7aa5 100644 --- a/src/components/microservices/code-queue/src/runtime-preflight.ts +++ b/src/components/microservices/code-queue/src/runtime-preflight.ts @@ -9,6 +9,8 @@ export interface RuntimePreflightOptions { includeRemote?: boolean; includePushDryRun?: boolean; pushDryRunRef?: string; + includePrCreateDryRun?: boolean; + prCreateDryRunHead?: string; issueNumber?: number; } @@ -115,6 +117,7 @@ export interface PullRequestDeliveryPreflight { ghPrReadOnly: RuntimeCommandProbe | null; }; pushDryRun?: RuntimeCommandProbe; + prCreateDryRun?: RuntimeCommandProbe; limitations: string[]; risks: string[]; } @@ -309,6 +312,55 @@ function httpsGitUrl(repo: string | null, host: string): string | null { return repo === null ? null : `https://${host}/${repo}.git`; } +function prCreateDryRunProbe(repo: string | null, issueNumber: number, head: string | null): RuntimeCommandProbe { + if (repo === null) { + return shellProbe("echo 'cannot run PR create dry-run: GitHub repo is unknown' >&2; exit 2", 2000); + } + const headRef = head?.trim() || "probe/code-queue-pr-capability"; + const body = [ + "# Code Queue PR preflight", + "", + `Related issue: #${issueNumber}`, + "", + "Validation:", + "- runtime PR create dry-run only", + "", + "Risk:", + "- no remote PR is created by this probe", + "", + ].join("\n"); + const script = [ + "set -eu", + "root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)", + "body_file=$(mktemp /tmp/unidesk-pr-create-dry-run.XXXXXX.md)", + "trap 'rm -f \"$body_file\"' EXIT", + `cat > "$body_file" <<'EOF_PR_PREFLIGHT_BODY'`, + body, + "EOF_PR_PREFLIGHT_BODY", + "cd \"$root\"", + "test -f scripts/cli.ts", + [ + "bun", + "scripts/cli.ts", + "gh", + "pr", + "create", + "--repo", + shellQuote(repo), + "--title", + shellQuote("Code Queue PR preflight dry run"), + "--body-file", + "\"$body_file\"", + "--base", + "master", + "--head", + shellQuote(headRef), + "--dry-run", + ].join(" "), + ].join("\n"); + return shellProbe(script, 20_000); +} + function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, checkedAt: string): PullRequestDeliveryPreflight { const tools = { git: toolStatus("git", ["--version"]), @@ -392,6 +444,10 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c const pushDryRun = options.includePushDryRun === true ? commandProbe("git", ["push", "--dry-run", "origin", `HEAD:${pushDryRunRef}`], { timeoutMs: 30_000 }) : undefined; + const prCreateDryRunHead = options.prCreateDryRunHead?.trim() || gitInfo.branch || "probe/code-queue-pr-capability"; + const prCreateDryRun = options.includePrCreateDryRun === true + ? prCreateDryRunProbe(githubRepo, issueProbeNumber, prCreateDryRunHead) + : undefined; const limitations: string[] = []; const risks: string[] = []; if (!tools.git.ok) limitations.push("missing git CLI"); @@ -419,12 +475,14 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c if (tools.gh.ok && remote.ghPrReadOnly !== null && !remote.ghPrReadOnly.ok) limitations.push("gh pr read-only probe failed"); } if (pushDryRun !== undefined && !pushDryRun.ok) limitations.push("git push --dry-run failed for probe branch"); + if (prCreateDryRun !== undefined && !prCreateDryRun.ok) limitations.push("PR create dry-run body/command guard failed"); const prCliReady = tools.gh.ok || tools.hub.ok; const tokenOrGhStore = credentials.ghTokenPresent || credentials.githubTokenPresent || credentials.ghHostsConfigPresent || (remote?.ghAuthStatus?.ok === true); const remoteReady = remote === undefined || (egress.githubDefault.ok && egress.apiDefault.ok && remote.gitLsRemote.ok && remote.githubSshAuthenticated); const pushReady = pushDryRun === undefined || pushDryRun.ok; + const prCreateDryRunReady = prCreateDryRun === undefined || prCreateDryRun.ok; return { - ok: tools.git.ok && tools.jq.ok && prCliReady && tokenOrGhStore && gitInfo.insideWorktree && gitInfo.remoteOrigin !== null && gitInfo.homeWritable && remoteReady && pushReady, + ok: tools.git.ok && tools.jq.ok && prCliReady && tokenOrGhStore && gitInfo.insideWorktree && gitInfo.remoteOrigin !== null && gitInfo.homeWritable && remoteReady && pushReady && prCreateDryRunReady, checkedAt, tools, credentials, @@ -438,6 +496,7 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c git: gitInfo, remote, pushDryRun, + prCreateDryRun, limitations, risks, };