fix: tighten code queue pr preflight contract
This commit is contained in:
@@ -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 <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 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/<name>] [--pr-create-dry-run --pr-create-dry-run-head <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 pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--pr-create-dry-run --pr-create-dry-run-head <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`。`preflight.prCapabilityContract` 是 runner-facing 合同摘要,必须包含目标分支、token 来源 key、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff 和 `gh pr merge` 的 `unsupported-command` 边界。`--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 <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 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`。
|
||||
|
||||
@@ -136,7 +136,7 @@ Runner preflight 优先使用执行面诊断入口:
|
||||
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。需要复核 PR body/创建命令 guard 时追加 `--pr-create-dry-run --pr-create-dry-run-head <head>`;该 guard 只执行 dry-run,不创建 PR。缺少 env token 时必须返回 `ok=false`、`runnerDisposition=infra-blocked` 和 `tokenCoverage.missing=["GH_TOKEN","GITHUB_TOKEN"]`,因为 provider dev container 只能转发 scheduler 已经拥有的 token。
|
||||
该命令经 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。需要复核 PR body/创建命令 guard 时追加 `--pr-create-dry-run --pr-create-dry-run-head <head>`;该 guard 只执行 dry-run,不创建 PR。缺少 env token 时必须返回 `ok=false`、`runnerDisposition=infra-blocked` 和 `tokenCoverage.missing=["GH_TOKEN","GITHUB_TOKEN"]`,因为 provider dev container 只能转发 scheduler 已经拥有的 token。输出中的 `prCapabilityContract` 用于指挥官快速审查 runner handoff:目标分支固定显示、push/PR create dry-run 标记为不写远端、系统 `gh` binary 与 UniDesk REST `bun scripts/cli.ts gh` 可用性分开报告,且 merge 明确保持 `unsupported-command`。
|
||||
|
||||
本地 runner preflight 示例:
|
||||
|
||||
|
||||
@@ -70,7 +70,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/<name>] [--pr-create-dry-run --pr-create-dry-run-head <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 read/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 能力属于执行面环境。
|
||||
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>] [--pr-create-dry-run --pr-create-dry-run-head <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 read/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 内容,并通过 `prCapabilityContract` 明确 token source、target branch、expected PR handoff、dry-run 不写远端和 merge unsupported 边界。backend-core 的稳定 `code-queue` proxy 必须把 `/api/runtime-preflight` 路由到 D601 scheduler,而不是主 server `code-queue-mgr`,因为 token 和 PR runner 能力属于执行面环境。
|
||||
|
||||
PR 创建依赖以下最小运行时能力:
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ function asRecord(value: unknown): JsonRecord {
|
||||
return value as JsonRecord;
|
||||
}
|
||||
|
||||
function fixtureRuntimePreflight(tokenPresent: boolean): JsonRecord {
|
||||
function fixtureRuntimePreflight(tokenPresent: boolean, overrides: { systemGhPresent?: boolean } = {}): JsonRecord {
|
||||
const systemGhPresent = overrides.systemGhPresent ?? true;
|
||||
return {
|
||||
ok: tokenPresent,
|
||||
checkedAt: "2026-05-20T00:00:00.000Z",
|
||||
@@ -26,12 +27,17 @@ function fixtureRuntimePreflight(tokenPresent: boolean): JsonRecord {
|
||||
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" },
|
||||
gh: { ok: systemGhPresent, path: systemGhPresent ? "/usr/bin/gh" : null, version: systemGhPresent ? "gh version 2.45.0" : null },
|
||||
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" },
|
||||
},
|
||||
unideskGhCli: {
|
||||
ok: true,
|
||||
path: "/workspace/unidesk/scripts/cli.ts",
|
||||
present: true,
|
||||
},
|
||||
credentials: {
|
||||
ghTokenPresent: tokenPresent,
|
||||
githubTokenPresent: false,
|
||||
@@ -68,18 +74,18 @@ function fixtureRuntimePreflight(tokenPresent: boolean): JsonRecord {
|
||||
},
|
||||
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: [],
|
||||
risks: systemGhPresent ? [] : ["system gh binary is missing; UniDesk REST gh CLI remains the supported PR create/comment path when scripts/cli.ts and GH_TOKEN/GITHUB_TOKEN are available"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixtureResponse(tokenPresent: boolean): JsonRecord {
|
||||
function fixtureResponse(tokenPresent: boolean, overrides: { systemGhPresent?: boolean } = {}): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
ok: true,
|
||||
runtimePreflight: fixtureRuntimePreflight(tokenPresent),
|
||||
runtimePreflight: fixtureRuntimePreflight(tokenPresent, overrides),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -96,6 +102,7 @@ export function runCodeQueuePrPreflightContract(): JsonRecord {
|
||||
{ observedPath },
|
||||
);
|
||||
assertCondition(asRecord(missing).ok === false, "missing token preflight should set top-level ok=false", missing);
|
||||
assertCondition(asRecord(missing).runnerDisposition === "infra-blocked", "missing token preflight should expose root runnerDisposition", 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);
|
||||
@@ -110,10 +117,21 @@ export function runCodeQueuePrPreflightContract(): JsonRecord {
|
||||
});
|
||||
const readyPreflight = asRecord(asRecord(ready).preflight);
|
||||
assertCondition(asRecord(ready).ok === true, "token-ready preflight should set top-level ok=true", ready);
|
||||
assertCondition(asRecord(ready).runnerDisposition === "ready", "token-ready preflight should expose root runnerDisposition", 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);
|
||||
const readyContract = asRecord(readyPreflight.prCapabilityContract);
|
||||
assertCondition(readyContract.targetBranch === "master", "PR preflight should expose target branch", readyContract);
|
||||
const readyPushDryRun = asRecord(readyContract.pushDryRun);
|
||||
assertCondition(readyPushDryRun.requested === true && readyPushDryRun.writesRemote === false, "push dry-run contract should be explicit and non-writing", readyPushDryRun);
|
||||
const readyHandoff = asRecord(readyContract.expectedPrHandoff);
|
||||
assertCondition(readyHandoff.targetBranch === "master" && readyHandoff.commanderReviewsAndMerges === true && readyHandoff.preflightCreatesPr === false, "PR handoff should stop at runner PR creation evidence", readyHandoff);
|
||||
const readyMergeBoundary = asRecord(readyContract.unsupportedMergeBoundary);
|
||||
assertCondition(readyMergeBoundary.degradedReason === "unsupported-command" && readyMergeBoundary.runnerDisposition === "business-failed", "merge boundary should remain unsupported", readyMergeBoundary);
|
||||
const readyCliStatus = asRecord(readyContract.unideskGhCli);
|
||||
assertCondition(readyCliStatus.ok === true && readyCliStatus.requiresSystemGhBinary === false, "UniDesk gh CLI availability should be separate from system gh binary", readyCliStatus);
|
||||
|
||||
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 });
|
||||
@@ -122,6 +140,19 @@ export function runCodeQueuePrPreflightContract(): JsonRecord {
|
||||
const prCreateDryRunPreflight = asRecord(asRecord(prCreateDryRun).preflight);
|
||||
const prCreateDryRunProbe = asRecord(prCreateDryRunPreflight.prCreateDryRun);
|
||||
assertCondition(prCreateDryRunProbe.ok === true, "PR create dry-run probe should be compacted", prCreateDryRunProbe);
|
||||
const prCreateDryRunContract = asRecord(prCreateDryRunPreflight.prCapabilityContract);
|
||||
const prCreateDryRunSummary = asRecord(prCreateDryRunContract.prCreateDryRun);
|
||||
assertCondition(prCreateDryRunSummary.requested === true && prCreateDryRunSummary.headBranch === "codequeue/pr-probe" && prCreateDryRunSummary.writesRemote === false, "PR create dry-run contract should be explicit and non-writing", prCreateDryRunSummary);
|
||||
|
||||
const noSystemGh = codexPrPreflightQueryForTest(["--remote"], (path) => {
|
||||
assertCondition(path === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1", "system gh missing fixture should use remote preflight path", { path });
|
||||
return fixtureResponse(true, { systemGhPresent: false });
|
||||
});
|
||||
const noSystemGhPreflight = asRecord(asRecord(noSystemGh).preflight);
|
||||
assertCondition(noSystemGhPreflight.ok === true, "missing system gh should not block UniDesk REST PR CLI when token and scripts/cli.ts exist", noSystemGhPreflight);
|
||||
const noSystemGhTools = asRecord(noSystemGhPreflight.tools);
|
||||
assertCondition(asRecord(noSystemGhTools.systemGhBinary).ok === false, "system gh binary status should be explicit", noSystemGhTools);
|
||||
assertCondition(asRecord(noSystemGhTools.unideskGhCli).ok === true, "UniDesk gh CLI should stay available when system gh is missing", noSystemGhTools);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -129,8 +160,12 @@ export function runCodeQueuePrPreflightContract(): JsonRecord {
|
||||
"stable runtime-preflight proxy path",
|
||||
"missing GitHub token is infra-blocked",
|
||||
"token key names are reported without values",
|
||||
"fake token source reports only the env key",
|
||||
"system gh binary and UniDesk REST gh CLI are distinct",
|
||||
"push dry-run options are forwarded",
|
||||
"PR create dry-run options are forwarded",
|
||||
"dry-run push and PR create are marked non-writing",
|
||||
"expected PR handoff and unsupported merge boundary are explicit",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2265,6 +2265,17 @@ function compactToolStatus(value: unknown): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function compactUniDeskGhCliStatus(value: unknown): Record<string, unknown> {
|
||||
const cli = asRecord(value) ?? {};
|
||||
return {
|
||||
ok: cli.ok ?? false,
|
||||
path: cli.path ?? null,
|
||||
present: cli.present ?? false,
|
||||
role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh",
|
||||
requiresSystemGhBinary: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactAgentPortStatus(value: unknown): Record<string, unknown> {
|
||||
const port = asRecord(value) ?? {};
|
||||
return {
|
||||
@@ -2298,6 +2309,7 @@ function tokenCoverageStatus(credentials: Record<string, unknown>): Record<strin
|
||||
function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> {
|
||||
const pull = asRecord(preflight.pullRequestDelivery) ?? {};
|
||||
const tools = asRecord(pull.tools) ?? {};
|
||||
const unideskGhCli = compactUniDeskGhCliStatus(pull.unideskGhCli);
|
||||
const credentials = asRecord(pull.credentials) ?? {};
|
||||
const git = asRecord(pull.git) ?? {};
|
||||
const githubContext = asRecord(pull.githubContext) ?? {};
|
||||
@@ -2309,6 +2321,10 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
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 defaultPushDryRunRef = "refs/heads/probe/code-queue-pr-capability-dryrun";
|
||||
const pushDryRunRef = options.pushDryRunRef ?? defaultPushDryRunRef;
|
||||
const targetBranch = "master";
|
||||
const expectedHeadBranch = options.prCreateDryRunHead ?? (typeof git.branch === "string" && git.branch.length > 0 ? git.branch : "<head-branch>");
|
||||
const result: Record<string, unknown> = {
|
||||
ok,
|
||||
checkedAt: preflight.checkedAt ?? pull.checkedAt ?? null,
|
||||
@@ -2320,13 +2336,48 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
pid: preflight.pid ?? null,
|
||||
},
|
||||
tokenCoverage,
|
||||
prCapabilityContract: {
|
||||
targetBranch,
|
||||
tokenSource: tokenCoverage.source,
|
||||
systemGhBinaryRequiredForWrites: false,
|
||||
unideskGhCli,
|
||||
pushDryRun: {
|
||||
requested: options.pushDryRun,
|
||||
ref: pushDryRunRef,
|
||||
writesRemote: false,
|
||||
commandShape: `git push --dry-run origin HEAD:${pushDryRunRef}`,
|
||||
},
|
||||
prCreateDryRun: {
|
||||
requested: options.prCreateDryRun,
|
||||
headBranch: expectedHeadBranch,
|
||||
writesRemote: false,
|
||||
commandShape: `bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --base ${targetBranch} --head ${expectedHeadBranch} --dry-run`,
|
||||
},
|
||||
expectedPrHandoff: {
|
||||
sourceBranch: expectedHeadBranch,
|
||||
targetBranch,
|
||||
runnerCreatesPrAfterAuthorization: true,
|
||||
commanderReviewsAndMerges: true,
|
||||
preflightCreatesPr: false,
|
||||
preflightMergesPr: false,
|
||||
},
|
||||
unsupportedMergeBoundary: {
|
||||
supported: false,
|
||||
command: "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk",
|
||||
degradedReason: "unsupported-command",
|
||||
runnerDisposition: "business-failed",
|
||||
note: "UniDesk CLI intentionally does not merge PRs in this phase; runner handoff stops at PR creation and evidence.",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
git: compactToolStatus(tools.git),
|
||||
gh: compactToolStatus(tools.gh),
|
||||
systemGhBinary: compactToolStatus(tools.gh),
|
||||
hub: compactToolStatus(tools.hub),
|
||||
jq: compactToolStatus(tools.jq),
|
||||
ssh: compactToolStatus(tools.ssh),
|
||||
curl: compactToolStatus(tools.curl),
|
||||
unideskGhCli,
|
||||
},
|
||||
agentPorts: {
|
||||
codex: compactAgentPortStatus(ports.codex),
|
||||
@@ -2404,6 +2455,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseF
|
||||
const compact = compactPrRuntimePreflight(preflight, options);
|
||||
return {
|
||||
ok: compact.ok,
|
||||
runnerDisposition: compact.runnerDisposition,
|
||||
upstream: response.upstream,
|
||||
preflight: compact,
|
||||
};
|
||||
|
||||
@@ -55,10 +55,17 @@ export interface RuntimePreflightReport {
|
||||
pullRequestDelivery: PullRequestDeliveryPreflight;
|
||||
}
|
||||
|
||||
export interface UniDeskGhCliStatus {
|
||||
ok: boolean;
|
||||
path: string;
|
||||
present: boolean;
|
||||
}
|
||||
|
||||
export interface PullRequestDeliveryPreflight {
|
||||
ok: boolean;
|
||||
checkedAt: string;
|
||||
tools: Record<string, RuntimeToolStatus>;
|
||||
unideskGhCli: UniDeskGhCliStatus;
|
||||
credentials: {
|
||||
ghTokenPresent: boolean;
|
||||
githubTokenPresent: boolean;
|
||||
@@ -235,6 +242,13 @@ function fileExistsProbe(path: string): boolean {
|
||||
return shellProbe(`test -s ${shellQuote(path)}`, 2000).ok;
|
||||
}
|
||||
|
||||
function unideskGhCliStatus(): UniDeskGhCliStatus {
|
||||
const root = gitValue(["rev-parse", "--show-toplevel"]) ?? process.cwd();
|
||||
const path = `${root.replace(/\/+$/u, "")}/scripts/cli.ts`;
|
||||
const present = fileExistsProbe(path);
|
||||
return { ok: present, path, present };
|
||||
}
|
||||
|
||||
function parseGitHubRepo(remote: string | null): string | null {
|
||||
const envRepo = process.env.GH_REPO?.trim();
|
||||
if (envRepo !== undefined && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(envRepo)) return envRepo;
|
||||
@@ -370,6 +384,7 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c
|
||||
ssh: toolStatus("ssh", ["-V"]),
|
||||
curl: toolStatus("curl", ["--version"]),
|
||||
};
|
||||
const unideskGhCli = unideskGhCliStatus();
|
||||
const insideProbe = commandProbe("git", ["rev-parse", "--is-inside-work-tree"], { timeoutMs: 5000 });
|
||||
const insideWorktree = insideProbe.ok && firstLine(insideProbe.stdout) === "true";
|
||||
const home = process.env.HOME ?? null;
|
||||
@@ -452,7 +467,8 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c
|
||||
const risks: string[] = [];
|
||||
if (!tools.git.ok) limitations.push("missing git CLI");
|
||||
if (!tools.jq.ok) limitations.push("missing jq CLI");
|
||||
if (!tools.gh.ok && !tools.hub.ok) limitations.push("missing PR CLI: install gh or hub");
|
||||
if (!tools.gh.ok) risks.push("system gh binary is missing; UniDesk REST gh CLI remains the supported PR create/comment path when scripts/cli.ts and GH_TOKEN/GITHUB_TOKEN are available");
|
||||
if (!unideskGhCli.ok) limitations.push("UniDesk gh CLI is missing at scripts/cli.ts; runner cannot use the repo-native REST PR create/comment path");
|
||||
if (!credentials.ghTokenPresent && !credentials.githubTokenPresent) limitations.push("GH_TOKEN/GITHUB_TOKEN is not present; gh cannot create PRs unless another gh credential store is mounted");
|
||||
if (!credentials.ghHostsConfigPresent && !credentials.gitCredentialsPresent && !credentials.ghTokenPresent && !credentials.githubTokenPresent) limitations.push("no gh hosts config, git credentials file, GH_TOKEN, or GITHUB_TOKEN is visible for GitHub API/issue/PR access");
|
||||
if (!gitInfo.insideWorktree) limitations.push("current directory is not a Git worktree");
|
||||
@@ -476,7 +492,7 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c
|
||||
}
|
||||
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 prCliReady = unideskGhCli.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;
|
||||
@@ -485,6 +501,7 @@ function collectPullRequestDeliveryPreflight(options: RuntimePreflightOptions, c
|
||||
ok: tools.git.ok && tools.jq.ok && prCliReady && tokenOrGhStore && gitInfo.insideWorktree && gitInfo.remoteOrigin !== null && gitInfo.homeWritable && remoteReady && pushReady && prCreateDryRunReady,
|
||||
checkedAt,
|
||||
tools,
|
||||
unideskGhCli,
|
||||
credentials,
|
||||
githubContext: {
|
||||
host: githubHost,
|
||||
|
||||
Reference in New Issue
Block a user