From d4c6fadc490a62cfe5a2e0836a7c954353ffc4af Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 23 May 2026 01:06:09 +0000 Subject: [PATCH] fix: compact codex pr preflight output --- docs/reference/cli.md | 2 +- .../code-queue-pr-preflight-contract-test.ts | 148 ++++++++---- .../code-queue-runner-skills-contract-test.ts | 28 ++- scripts/src/code-queue.ts | 219 +++++++++++++++++- scripts/src/help.ts | 4 +- 5 files changed, 329 insertions(+), 72 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3002bd32..713c7c0c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -45,7 +45,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` 只返回结构化请求且不实际入队。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 必须优先用 `--prompt-stdin` 或 `--prompt-file`,不要拼进 shell 单个参数;位置参数只适合短单行 smoke prompt。stdin 推荐用 quoted heredoc:`cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run`,文件路径推荐 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`,确认 dry-run 后移除 `--dry-run` 提交同一 payload。dry-run 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/运行态或 release 禁止/证据要求/中等复杂度候选等 guard 状态;同时输出 `policyContract`,固定暴露 GPT-5.5、DeepSeek、MiniMax 的风险分层、并发上限和外部 provider 429 退避处置。该建议只用于指挥官 preflight,不会改写 payload,不改变 runtime admission,也不假设生产 MiniMax 或 DeepSeek 可用。`--dry-run` 必须返回完整 prompt、字符数和 `truncated=false` 用于人工验收;真实提交是写入操作,默认只返回 `accepted=true`、task id、队列、写入保护摘要和后续查看命令,必须标记 `promptOmitted=true` 且不得回显 prompt 或 promptPreview。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 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/] [--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 覆盖、Auth Broker source/capability/nextAction、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测、可选 push dry-run,以及可选 PR body/create dry-run guard;只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。当 auth-broker 配置存在时,`tokenCoverage.source="auth-broker"`、`credentialSource="broker-issued-token"` 且 runner env token 不是成功前提;当仅 env token 存在时,`credentialSource="env-token"` 且 `preflight.authBroker.nextAction="use-env-token-until-auth-broker-live"`;两者都缺失时顶层 `ok=false`、`runnerDisposition=infra-blocked`、`degradedReason=auth-broker-needed`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`,并输出 `preflight.authBroker.source="broker/auth-broker-needed"`、`capability.source="missing-token"`。该 `auth-missing` 的 scope 是 `scheduler-runner-env`,不能简化成“当前 active runner/dev container 不能创建 PR”;输出必须带 `scopeBoundary` 和 `activeRunnerDevContainer`,要求调用方另跑 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk` 与 PR dry-run 来确认当前 dev container 能力。GitHub DNS/API 连接失败应归类为 `failureKind=github-transient`、`degradedReason=github-dns-api-transient`,并带 `retryable=true`、`commanderAction=retry-backoff-or-keep-running-if-heartbeat-fresh` 和有界 `githubTransient.failedProbes`;调用方应重试/退避,且在任务 heartbeat/trace 新鲜时继续监督,不把它当成 auth 缺失或 PR 语义失败。`preflight.prCapabilityContract` 是 runner-facing 合同摘要,必须包含目标分支、token/auth 来源、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff、真实 PR 创建需要 commander 授权和 `gh pr merge` 的 `unsupported-command` 边界;系统 `gh` binary 缺失只进入 `tools.systemGhBinary`,不得误判为 UniDesk REST `gh` CLI 不可用。`--remote` 在 runner-like 环境里不再依赖本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些缺失只作为本地观测证据。若远程控制面可达,则继续走远程控制面结果;若远程控制面不可达,则结构化返回 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,而不是把本地 `backend-core-container-missing` 当作最终阻塞。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token/auth broker、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 +- `codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full|--raw]` 通过稳定 `code-queue` proxy 请求 D601 scheduler `/api/runtime-preflight`,用于 PR 型派单 admission。默认输出是紧凑 commander 视图,显式分出 `schedulerPreflight` 与 `activeRunnerPrCapability`,并附带 `commands` 和 `disclosure`,方便先看 scheduler auth 缺口、再看当前 runner/dev container 的 `gh auth status` 与 `gh pr create --dry-run` 能力;`--full` 或 `--raw` 才展开完整 `preflight`、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测和观测原文。只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。当 auth-broker 配置存在时,`tokenCoverage.source="auth-broker"`、`credentialSource="broker-issued-token"` 且 runner env token 不是成功前提;当仅 env token 存在时,`credentialSource="env-token"` 且 `authBroker.nextAction="use-env-token-until-auth-broker-live"`;两者都缺失时顶层 `ok=false`、`runnerDisposition=infra-blocked`、`degradedReason=auth-broker-needed`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`,并输出 `authBroker.source="broker/auth-broker-needed"`、`capability.source="missing-token"`。该 `auth-missing` 的 scope 是 `scheduler-runner-env`,不能简化成“当前 active runner/dev container 不能创建 PR”;默认视图必须带 `scopeBoundary` 和 `activeRunnerPrCapability`。GitHub DNS/API 连接失败应归类为 `failureKind=github-transient`、`degradedReason=github-dns-api-transient`,并带 `retryable=true`、`commanderAction=retry-backoff-or-keep-running-if-heartbeat-fresh` 和有界 `githubTransient.failedProbes`;调用方应重试/退避,且在任务 heartbeat/trace 新鲜时继续监督,不把它当成 auth 缺失或 PR 语义失败。`prCapability` 是 runner-facing 合同摘要,必须包含目标分支、token/auth 来源、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff、真实 PR 创建需要 commander 授权和 `gh pr merge` 的 `unsupported-command` 边界;系统 `gh` binary 缺失只进入 `tools.systemGhBinary`,不得误判为 UniDesk REST `gh` CLI 不可用。`--remote` 在 runner-like 环境里不再依赖本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些缺失只作为本地观测证据。若远程控制面可达,则继续走远程控制面结果;若远程控制面不可达,则结构化返回 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,而不是把本地 `backend-core-container-missing` 当作最终阻塞。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token/auth broker、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。`--detail` 仍是有界详细摘要:默认只返回少量 attempt/tool 行、短 prompt/response/stderr/feedback 预览和 omitted/truncated 元数据;需要完整 prompt/response 文本或更多 tool/attempt 细节时再显式加 `--full`、`--tool-limit N`、`--trace` 或 `codex output`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 - `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` 是低噪声指挥官视图,只返回 `activeRunning`、`running`、`completedUnread`、`recentCompleted`、`queued`、`activity`、`commanderConcurrency` 和 `executionDiagnostics` 的紧凑行;`activeRunning.count` 是 running+judging 的状态计数,`exact=true` 时来自 queue summary counts,`running.returned` 和 `activeRunning.rowPage.returned` 只是本次返回的紧凑行数。`commanderConcurrency.activeRunnerCount` 是并发策略应使用的 active/running 计数,等于 `activity.effectiveActiveTaskCount`;15 并发策略按 `15 - activeRunnerCount` 计算剩余窗口。`commanderConcurrency.splitBrainDisposition=live-count-as-active` 表示 split-brain 有 fresh heartbeat 证据,应继续监督并计入 active;`interventionRequired=true` 才提示介入。prompt/body 只给短预览和原始字符数,`running`/`completedUnread`/`queued` 默认只返回一个有界小页并通过 section `commands.next` 继续分页,`recentCompleted` 默认限量且不重复 `completedUnread` 未读终态,不嵌入完整 Trace、final response 或全量 overview。`--limit` 在 supervisor 中主要是扫描/分页预算,不是返回几十条肥行的开关;CLI 安全上限是 100,输出会在 `filters.requestedLimit`、`filters.effectiveLimit`、`filters.limitCapped` 和 `disclosure.limitPolicy` 说明显式请求是否被 capped;底层 overview 拉取预算独立显示在 `source.requestedLimit` / `source.effectiveLimit`,所以 `--limit 260` 应显示 requested=260、effective=100、source requested/effective=200,而不是只露出一个含糊的 `limit`。`--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/scripts/code-queue-pr-preflight-contract-test.ts b/scripts/code-queue-pr-preflight-contract-test.ts index 68c63bf0..86677bb0 100644 --- a/scripts/code-queue-pr-preflight-contract-test.ts +++ b/scripts/code-queue-pr-preflight-contract-test.ts @@ -347,10 +347,11 @@ async function main(): Promise { assertCondition(asRecord(fallback.disclosure).fullObservationsOmitted === true, "healthy remote fallback should disclose omitted full observations", fallback.disclosure); assertCondition(asRecord(fallback.localObservationSummary).failureKind === "target-stack-not-running", "healthy remote fallback should keep bounded local observation summary", fallback.localObservationSummary); assertCondition(asRecord(fallback.remoteObservationSummary).ok === true, "healthy remote fallback should keep bounded remote observation summary", fallback.remoteObservationSummary); - const fallbackPreflight = asRecord(fallback.preflight); - assertCondition(fallbackPreflight.ok === true, "remote fallback preflight should stay ready", fallbackPreflight); - assertCondition(asRecord(fallbackPreflight.tokenCoverage).source === "GH_TOKEN", "token source should be GH_TOKEN", fallbackPreflight.tokenCoverage); - assertCondition(asRecord(fallbackPreflight.prCapabilityContract).targetBranch === "master", "target branch should stay master", fallbackPreflight.prCapabilityContract); + assertCondition(fallback.preflight === undefined, "remote fallback default output should omit detailed preflight", fallback); + const fallbackSchedulerPreflight = asRecord(fallback.schedulerPreflight); + assertCondition(fallbackSchedulerPreflight.authReady === true, "remote fallback scheduler summary should stay ready", fallbackSchedulerPreflight); + assertCondition(fallbackSchedulerPreflight.authSource === "GH_TOKEN", "token source should be GH_TOKEN", fallbackSchedulerPreflight); + assertCondition(asRecord(fallback.prCapability).targetBranch === "master", "target branch should stay master", fallback.prCapability); const authMissing = await codexPrPreflightQueryForTest(["--remote"], { config: null, @@ -406,23 +407,26 @@ async function main(): Promise { assertCondition(directAuthObservationGap.kind === "runner-local-observation-gap", "auth missing after remote fallback should keep local backend-core absence scoped as runner-local observation gap", directAuthObservationGap); const directAuthSummary = asRecord(directAuthMissingRecord.authScopeSummary); const directAuthScopeBoundary = asRecord(directAuthMissingRecord.scopeBoundary); - const directAuthActiveRunner = asRecord(directAuthMissingRecord.activeRunnerDevContainer); + const directAuthActiveRunner = asRecord(directAuthMissingRecord.activeRunnerPrCapability); const directAuthRecommendedActions = Array.isArray(directAuthMissingRecord.recommendedActions) ? directAuthMissingRecord.recommendedActions : []; assertCondition(directAuthSummary.schedulerAuthMissingIsScoped === true, "remote auth-missing should lead with scheduler-scoped auth summary", directAuthSummary); assertCondition(String(directAuthSummary.interpretation ?? "").includes("does not prove"), "remote auth summary must not imply active runner PR incapability", directAuthSummary); assertCondition(directAuthScopeBoundary.scopesAreIndependent === true, "remote auth-missing must distinguish scheduler env from active runner dev container", directAuthScopeBoundary); assertCondition(directAuthScopeBoundary.schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr === true, "remote auth-missing should expose the explicit PR capability boundary", directAuthScopeBoundary); assertCondition(String(directAuthScopeBoundary.authMissingInterpretation ?? "").includes("do not simplify"), "remote auth-missing must warn against overbroad interpretation", directAuthScopeBoundary); - assertCondition(directAuthActiveRunner.notEquivalentToSchedulerEnv === true, "active runner token capability must be a separate scope", directAuthActiveRunner); + assertCondition(directAuthActiveRunner.independentOfSchedulerPreflight === true, "active runner token capability must be a separate scope", directAuthActiveRunner); assertCondition(Array.isArray(directAuthMissingRecord.recommendedActions), "remote auth-missing should expose bounded recommended actions", directAuthMissingRecord.recommendedActions); assertCondition(directAuthRecommendedActions.length === 3, "remote auth-missing recommended actions should stay bounded", directAuthRecommendedActions); assertCondition(directAuthRecommendedActions.some((action) => asRecord(action).action === "verify-current-runner-auth"), "remote auth-missing should recommend active runner auth status first", directAuthRecommendedActions); assertCondition(directAuthRecommendedActions.some((action) => String(asRecord(action).command ?? "").includes("gh pr create") && asRecord(action).writesRemote === false), "remote auth-missing should recommend PR create dry-run", directAuthRecommendedActions); assertCondition(directAuthMissingRecord.localObservation === undefined, "auth-missing remote fallback default output should omit full local observation", directAuthMissingRecord); assertCondition(directAuthMissingRecord.remoteObservation === undefined, "auth-missing remote fallback default output should omit full remote observation", directAuthMissingRecord); + assertCondition(directAuthMissingRecord.preflight === undefined, "auth-missing remote fallback default output should omit detailed preflight", directAuthMissingRecord); assertCondition(asRecord(directAuthMissingRecord.disclosure).fullObservationsOmitted === true, "auth-missing remote fallback should disclose omitted full observations", directAuthMissingRecord.disclosure); + assertCondition(asRecord(directAuthMissingRecord.disclosure).fullDetailOmitted === true, "auth-missing remote fallback should disclose omitted full detail", directAuthMissingRecord.disclosure); assertCondition(asRecord(directAuthMissingRecord.localObservationSummary).failureKind === "target-stack-not-running", "auth-missing remote fallback should keep bounded local observation summary", directAuthMissingRecord.localObservationSummary); assertCondition(asRecord(asRecord(directAuthMissingRecord.remoteObservationSummary).tokenCoverage).scope === "scheduler-runner-env", "auth-missing remote fallback should keep bounded remote token scope", directAuthMissingRecord.remoteObservationSummary); + assertCondition(JSON.stringify(directAuthMissingRecord).length < 12000, "auth-missing remote fallback default output should stay compact", { chars: JSON.stringify(directAuthMissingRecord).length }); const directAuthMissingFull = await codexPrPreflightQueryForTest(["--remote", "--full"], { config: { network: { publicHost: "74.48.78.17", frontend: { port: 18081 } } } as unknown as UniDeskConfig, @@ -599,15 +603,63 @@ async function main(): Promise { assertCondition(githubTransientRecord.runnerDisposition === "infra-blocked", "GitHub transient keeps infra-blocked disposition for legacy callers", githubTransientRecord); assertCondition(githubTransientRecord.retryable === true, "GitHub transient should expose top-level retryable=true", githubTransientRecord); assertCondition(githubTransientRecord.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient should expose top-level commander action", githubTransientRecord); - const githubTransientPreflight = asRecord(githubTransientRecord.preflight); - assertCondition(githubTransientPreflight.retryable === true, "GitHub transient preflight should be retryable", githubTransientPreflight); - assertCondition(githubTransientPreflight.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient should expose bounded commander action", githubTransientPreflight); - const githubTransient = asRecord(githubTransientPreflight.githubTransient); + assertCondition(githubTransientRecord.preflight === undefined, "GitHub transient default output should omit detailed preflight", githubTransientRecord); + const githubTransient = asRecord(githubTransientRecord.githubTransient); assertCondition(githubTransient.kind === "github-transient", "GitHub transient evidence should identify kind", githubTransient); assertCondition(githubTransient.notAuthMissing === true, "GitHub transient must not be auth-missing", githubTransient); assertCondition(githubTransient.notPrSemanticFailure === true, "GitHub transient must not be PR semantic failure", githubTransient); assertCondition(Array.isArray(githubTransient.failedProbes) && githubTransient.failedProbes.length <= 4, "GitHub transient evidence should stay bounded", githubTransient); assertCondition(String(githubTransient.commanderAction ?? "").includes("keep the task running"), "GitHub transient action should preserve fresh-heartbeat tasks", githubTransient); + const githubTransientFullRecord = asRecord(await codexPrPreflightQueryForTest(["--remote", "--full"], { + config: null, + coreFetch: () => ({ + ok: true, + status: 200, + body: { + runtimePreflight: rawRuntimePreflightFixture({ + ok: false, + pullRequestDelivery: { + ...asRecord(rawRuntimePreflightFixture().pullRequestDelivery), + ok: false, + credentials: { + ghTokenPresent: true, + githubTokenPresent: false, + ghHostsConfigPresent: false, + gitCredentialsPresent: false, + }, + egress: { + proxy: { + selectedProxyHost: "d601-provider-egress-proxy.unidesk.svc.cluster.local", + selectedProxyPort: "18789", + selectedProxyHostResolvable: true, + }, + githubDefault: { command: "curl", args: ["-IsS", "https://github.com"], ok: false, exitCode: 6, signal: null, error: null, stdout: "", stderr: "curl: (6) Could not resolve host: github.com" }, + apiDefault: { command: "curl", args: ["-IsS", "https://api.github.com"], ok: false, exitCode: 6, signal: null, error: null, stdout: "", stderr: "curl: (6) Could not resolve host: api.github.com" }, + issueApi: null, + }, + remote: { + gitLsRemote: { command: "git", args: ["ls-remote", "--heads", "origin", "master"], ok: false, exitCode: 128, signal: null, error: null, stdout: "", stderr: "ssh: Could not resolve hostname github.com: Temporary failure in name resolution" }, + gitHttpsLsRemote: null, + githubSshAuthenticated: false, + ghAuthStatus: { command: "gh", args: ["auth", "status"], ok: false, exitCode: 1, signal: null, error: null, stdout: "", stderr: "error connecting to api.github.com" }, + ghRepoView: null, + ghIssueView: null, + ghPrReadOnly: null, + }, + limitations: [ + "GitHub HTTPS probe failed with the default environment/proxy", + "GitHub API probe failed with the default environment/proxy", + "git ls-remote origin master failed", + ], + risks: [], + }, + }), + }, + }), + }), "github transient full record"); + const githubTransientPreflight = asRecord(githubTransientFullRecord.preflight); + assertCondition(githubTransientPreflight.retryable === true, "GitHub transient full preflight should be retryable", githubTransientPreflight); + assertCondition(githubTransientPreflight.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient full preflight should expose bounded commander action", githubTransientPreflight); let observedDryRunPath = ""; const dryRunContract = await codexPrPreflightQueryForTest([ @@ -707,24 +759,23 @@ async function main(): Promise { assertCondition(observedDryRunPath === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&pushDryRun=1&pushDryRunRef=refs%2Fheads%2Fprobe%2Fcode-queue-pr-capability&prCreateDryRun=1&prCreateDryRunHead=code-queue%2Fissue-35-pr-dry-run-probe&issue=20", "combined dry-run query should pass all requested guards", { observedDryRunPath }); const dryRunRecord = asRecord(dryRunContract); assertCondition(dryRunRecord.failureKind === "auth-missing", "missing runner token should remain auth-missing even when system gh is absent", dryRunRecord); - const dryRunPreflight = asRecord(dryRunRecord.preflight); - const dryRunAuthBroker = asRecord(dryRunPreflight.authBroker); - const dryRunScopeBoundary = asRecord(dryRunPreflight.scopeBoundary); - const dryRunActiveRunner = asRecord(dryRunPreflight.activeRunnerDevContainer); + assertCondition(dryRunRecord.preflight === undefined, "combined dry-run default output should omit detailed preflight", dryRunRecord); + const dryRunSchedulerPreflight = asRecord(dryRunRecord.schedulerPreflight); + const dryRunAuthBroker = asRecord(dryRunSchedulerPreflight.authBroker); + const dryRunScopeBoundary = asRecord(dryRunRecord.scopeBoundary); + const dryRunActiveRunner = asRecord(dryRunRecord.activeRunnerPrCapability); assertCondition(dryRunAuthBroker.source === "broker/auth-broker-needed", "missing runner token should expose broker/auth-broker-needed", dryRunAuthBroker); - assertCondition(dryRunAuthBroker.degradedReason === "auth-broker-needed", "auth broker degraded reason should be explicit", dryRunAuthBroker); + assertCondition(dryRunSchedulerPreflight.degradedReason === "auth-broker-needed", "auth broker degraded reason should be explicit", dryRunSchedulerPreflight); assertCondition(dryRunScopeBoundary.scopesAreIndependent === true, "local compact preflight should expose independent auth scopes", dryRunScopeBoundary); - assertCondition(dryRunActiveRunner.scope === "current-cli-process", "local compact preflight should expose current CLI process capability", dryRunActiveRunner); - const dryRunBrokerEvidence = asRecord(dryRunAuthBroker.evidence); - assertCondition(dryRunBrokerEvidence.systemGhBinaryOk === false, "system gh absence should be reported separately", dryRunBrokerEvidence); - assertCondition(dryRunBrokerEvidence.unideskGhCliOk === true, "UniDesk REST gh CLI should not be marked unavailable because system gh is missing", dryRunBrokerEvidence); - assertCondition(dryRunBrokerEvidence.systemGhMissingMisclassifiedAsUniDeskCliMissing === false, "system gh absence must not be misclassified", dryRunBrokerEvidence); - const dryRunPrContract = asRecord(dryRunPreflight.prCapabilityContract); + assertCondition(dryRunActiveRunner.scope === "current-cli-process", "compact default should expose current CLI process capability", dryRunActiveRunner); + const dryRunPrContract = asRecord(dryRunRecord.prCapability); + assertCondition(dryRunPrContract.systemGhBinaryRequiredForWrites === false, "system gh absence must not be required for UniDesk REST gh writes", dryRunPrContract); + assertCondition(dryRunPrContract.unideskGhCliOk === true, "UniDesk REST gh CLI should not be marked unavailable because system gh is missing", dryRunPrContract); assertCondition(asRecord(dryRunPrContract.pushDryRun).requested === true, "push dry-run should be requested", dryRunPrContract); assertCondition(asRecord(dryRunPrContract.pushDryRun).writesRemote === false, "push dry-run must be marked non-writing", dryRunPrContract); assertCondition(asRecord(dryRunPrContract.prCreateDryRun).requested === true, "PR create dry-run should be requested", dryRunPrContract); assertCondition(asRecord(dryRunPrContract.prCreateDryRun).writesRemote === false, "PR create dry-run must be marked non-writing", dryRunPrContract); - assertCondition(asRecord(dryRunPrContract.prCreateDryRun).headBranch === "code-queue/issue-35-pr-dry-run-probe", "PR dry-run head should come from the option", dryRunPrContract); + assertCondition(dryRunPrContract.sourceBranch === "code-queue/issue-35-pr-dry-run-probe", "PR dry-run head should come from the option", dryRunPrContract); const brokerIssuedContract = await codexPrPreflightQueryForTest(["--remote"], { config: null, @@ -768,18 +819,17 @@ async function main(): Promise { }); const brokerIssuedRecord = asRecord(brokerIssuedContract); assertCondition(brokerIssuedRecord.ok === true, "broker-issued token branch should be ready", brokerIssuedRecord); - const brokerIssuedPreflight = asRecord(brokerIssuedRecord.preflight); - const brokerIssuedTokenCoverage = asRecord(brokerIssuedPreflight.tokenCoverage); - const brokerIssuedAuthBroker = asRecord(brokerIssuedPreflight.authBroker); - const brokerIssuedCapability = asRecord(brokerIssuedAuthBroker.capability); - const brokerIssuedPrContract = asRecord(brokerIssuedPreflight.prCapabilityContract); - assertCondition(brokerIssuedTokenCoverage.source === "auth-broker", "broker-issued branch should use auth-broker token coverage", brokerIssuedTokenCoverage); - assertCondition(brokerIssuedTokenCoverage.credentialSource === "broker-issued-token", "broker-issued branch should expose broker-issued-token capability", brokerIssuedTokenCoverage); + assertCondition(brokerIssuedRecord.preflight === undefined, "broker-issued default output should omit detailed preflight", brokerIssuedRecord); + const brokerIssuedScheduler = asRecord(brokerIssuedRecord.schedulerPreflight); + const brokerIssuedAuthBroker = asRecord(brokerIssuedScheduler.authBroker); + const brokerIssuedPrContract = asRecord(brokerIssuedRecord.prCapability); + assertCondition(brokerIssuedScheduler.authSource === "auth-broker", "broker-issued branch should use auth-broker token coverage", brokerIssuedScheduler); + assertCondition(brokerIssuedScheduler.credentialSource === "broker-issued-token", "broker-issued branch should expose broker-issued-token capability", brokerIssuedScheduler); assertCondition(brokerIssuedAuthBroker.source === "auth-broker", "broker-issued branch should expose authBroker.source", brokerIssuedAuthBroker); assertCondition(brokerIssuedAuthBroker.nextAction === "use-auth-broker", "broker-issued branch should expose nextAction", brokerIssuedAuthBroker); - assertCondition(brokerIssuedCapability.systemGhBinaryRequiredForWrites === false, "broker-issued branch should not require system gh binary", brokerIssuedCapability); - assertCondition(brokerIssuedCapability.realPrCreateRequiresCommanderAuthorization === true, "real PR creation should still require commander authorization", brokerIssuedCapability); - assertCondition(asRecord(brokerIssuedPrContract.authBroker).source === "auth-broker", "PR capability should include broker source", brokerIssuedPrContract); + assertCondition(brokerIssuedAuthBroker.capabilitySource === "broker-issued-token", "broker-issued branch should expose broker-issued capability", brokerIssuedAuthBroker); + assertCondition(brokerIssuedPrContract.systemGhBinaryRequiredForWrites === false, "broker-issued branch should not require system gh binary", brokerIssuedPrContract); + assertCondition(brokerIssuedPrContract.realPrCreateRequiresCommanderAuthorization === true, "real PR creation should still require commander authorization", brokerIssuedPrContract); const envTokenContract = await codexPrPreflightQueryForTest(["--remote"], { config: null, @@ -829,11 +879,11 @@ async function main(): Promise { }); const envTokenRecord = asRecord(envTokenContract); assertCondition(envTokenRecord.ok === true, "env token branch should be ready", envTokenRecord); - const envTokenPreflight = asRecord(envTokenRecord.preflight); - const envTokenCoverage = asRecord(envTokenPreflight.tokenCoverage); - const envTokenAuthBroker = asRecord(envTokenPreflight.authBroker); - assertCondition(envTokenCoverage.source === "GH_TOKEN", "env token branch should expose GH_TOKEN source", envTokenCoverage); - assertCondition(envTokenCoverage.credentialSource === "env-token", "env token branch should expose env-token capability", envTokenCoverage); + assertCondition(envTokenRecord.preflight === undefined, "env-token default output should omit detailed preflight", envTokenRecord); + const envTokenScheduler = asRecord(envTokenRecord.schedulerPreflight); + const envTokenAuthBroker = asRecord(envTokenScheduler.authBroker); + assertCondition(envTokenScheduler.authSource === "GH_TOKEN", "env token branch should expose GH_TOKEN source", envTokenScheduler); + assertCondition(envTokenScheduler.credentialSource === "env-token", "env token branch should expose env-token capability", envTokenScheduler); assertCondition(envTokenAuthBroker.source === "GH_TOKEN", "env token branch should not pretend broker is configured", envTokenAuthBroker); assertCondition(envTokenAuthBroker.nextAction === "use-env-token-until-auth-broker-live", "env token branch should still point at broker migration", envTokenAuthBroker); @@ -852,25 +902,25 @@ async function main(): Promise { const missingTokenTopSummary = asRecord(missingTokenRecord.authScopeSummary); const missingTokenTopScopeBoundary = asRecord(missingTokenRecord.scopeBoundary); const missingTokenTopActions = Array.isArray(missingTokenRecord.recommendedActions) ? missingTokenRecord.recommendedActions : []; - const missingTokenPreflight = asRecord(missingTokenRecord.preflight); - const missingTokenAuthBroker = asRecord(missingTokenPreflight.authBroker); - const missingTokenSummary = asRecord(missingTokenPreflight.authScopeSummary); - const missingTokenScopeBoundary = asRecord(missingTokenPreflight.scopeBoundary); - const missingTokenActiveRunner = asRecord(missingTokenPreflight.activeRunnerDevContainer); - const missingTokenActions = Array.isArray(missingTokenPreflight.recommendedActions) ? missingTokenPreflight.recommendedActions : []; - const missingTokenCapability = asRecord(missingTokenAuthBroker.capability); + assertCondition(missingTokenRecord.preflight === undefined, "missing-token default output should omit detailed preflight", missingTokenRecord); + const missingTokenScheduler = asRecord(missingTokenRecord.schedulerPreflight); + const missingTokenAuthBroker = asRecord(missingTokenScheduler.authBroker); + const missingTokenScopeBoundary = asRecord(missingTokenRecord.scopeBoundary); + const missingTokenActiveRunner = asRecord(missingTokenRecord.activeRunnerPrCapability); + const missingTokenActions = Array.isArray(missingTokenRecord.recommendedActions) ? missingTokenRecord.recommendedActions : []; + const missingTokenPrCapability = asRecord(missingTokenRecord.prCapability); assertCondition(missingTokenTopSummary.schedulerAuthMissingIsScoped === true, "missing-token top-level summary should expose scoped scheduler auth missing", missingTokenTopSummary); assertCondition(missingTokenTopScopeBoundary.schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr === true, "missing-token top-level boundary should be prominent", missingTokenTopScopeBoundary); assertCondition(Array.isArray(missingTokenRecord.recommendedActions) && missingTokenTopActions.length === 3, "missing-token top-level recommended actions should stay bounded", missingTokenRecord.recommendedActions); assertCondition(missingTokenAuthBroker.source === "broker/auth-broker-needed", "missing-token branch should expose broker/auth-broker-needed", missingTokenAuthBroker); assertCondition(missingTokenAuthBroker.nextAction === "configure-auth-broker-or-env-token", "missing-token branch should expose nextAction", missingTokenAuthBroker); - assertCondition(missingTokenCapability.source === "missing-token", "missing-token branch should expose missing-token capability", missingTokenCapability); - assertCondition(missingTokenCapability.systemGhBinaryRequiredForWrites === false, "missing-token branch should still not require system gh binary for UniDesk gh CLI", missingTokenCapability); - assertCondition(missingTokenSummary.schedulerAuthMissingIsScoped === true, "missing-token compact preflight should expose scoped scheduler auth missing", missingTokenSummary); + assertCondition(missingTokenAuthBroker.capabilitySource === "missing-token", "missing-token branch should expose missing-token capability", missingTokenAuthBroker); + assertCondition(missingTokenPrCapability.systemGhBinaryRequiredForWrites === false, "missing-token branch should still not require system gh binary for UniDesk gh CLI", missingTokenPrCapability); + assertCondition(missingTokenTopSummary.schedulerAuthMissingIsScoped === true, "missing-token compact summary should expose scoped scheduler auth missing", missingTokenTopSummary); assertCondition(String(missingTokenScopeBoundary.currentRunnerCheck ?? "").includes("gh auth status"), "missing-token branch should point to active runner auth check", missingTokenScopeBoundary); assertCondition(String(missingTokenScopeBoundary.currentRunnerCheck ?? "").includes("gh pr create --dry-run"), "missing-token branch should point to active runner PR create dry-run", missingTokenScopeBoundary); - assertCondition(missingTokenActiveRunner.relationToRemotePreflight === "independent-scope; scheduler-runner-env auth-missing does not prove the active runner/dev container lacks GitHub PR capability", "missing-token branch should not overstate active runner PR capability", missingTokenActiveRunner); - assertCondition(Array.isArray(missingTokenPreflight.recommendedActions) && missingTokenActions.length === 3, "missing-token compact recommended actions should stay bounded", missingTokenPreflight.recommendedActions); + assertCondition(missingTokenActiveRunner.independentOfSchedulerPreflight === true, "missing-token branch should not overstate active runner PR capability", missingTokenActiveRunner); + assertCondition(Array.isArray(missingTokenRecord.recommendedActions) && missingTokenActions.length === 3, "missing-token compact recommended actions should stay bounded", missingTokenRecord.recommendedActions); assertCondition(missingTokenActions.some((action) => String(asRecord(action).command ?? "").includes("gh auth status")), "missing-token branch should recommend gh auth status", missingTokenActions); assertCondition(missingTokenActions.some((action) => String(asRecord(action).command ?? "").includes("gh pr create") && asRecord(action).writesRemote === false), "missing-token branch should recommend PR create dry-run without writes", missingTokenActions); diff --git a/scripts/code-queue-runner-skills-contract-test.ts b/scripts/code-queue-runner-skills-contract-test.ts index 14e00069..d21fc10a 100644 --- a/scripts/code-queue-runner-skills-contract-test.ts +++ b/scripts/code-queue-runner-skills-contract-test.ts @@ -118,7 +118,7 @@ assertCondition(microserviceCli.includes("compactSkillSync"), "microservice heal assertCondition(helpSource.includes("codex skills-sync --dry-run"), "CLI help must document the skills sync dry-run command"); assertCondition(docsReference.includes("codex skills-sync --dry-run"), "reference docs must document the skills sync dry-run command"); -const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], { +const skillsPreflightTransport = { config: null, coreFetch: () => ({ ok: true, @@ -169,18 +169,26 @@ const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], { }, }, }), -}), "preflight summary"); +}; +const defaultPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], skillsPreflightTransport), "default preflight summary"); +assertCondition(defaultPreflightSummary.failureKind === "runner-skills-blocker", "default preflight should classify missing skills as runner-skills-blocker", defaultPreflightSummary); +assertCondition(defaultPreflightSummary.degradedReason === "unapproved-target", "default preflight degraded reason should use the skills sync blocker first", defaultPreflightSummary); +assertCondition(defaultPreflightSummary.preflight === undefined, "default PR preflight should omit detailed preflight internals", defaultPreflightSummary); +assertCondition(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").fullDetailOmitted === true, "default PR preflight should disclose full detail omission", defaultPreflightSummary.disclosure); +assertCondition(String(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").expandWith ?? "").includes("--full"), "default PR preflight should point to --full expansion", defaultPreflightSummary.disclosure); + +const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote", "--full"], skillsPreflightTransport), "full preflight summary"); const preflight = asRecord(preflightSummary.preflight, "preflight"); const preflightSkills = asRecord(preflight.skills, "preflight.skills"); const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync"); -assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "compact preflight should classify missing skills as runner-skills-blocker", preflightSummary); -assertCondition(preflightSummary.degradedReason === "unapproved-target", "compact preflight degraded reason should use the skills sync blocker first", preflightSummary); -assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "compact preflight must show skills target", preflightSkills); -assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "compact preflight must show non-mutating skills sync dry-run", preflightSkillsSync); -assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "compact preflight must show missing target count", preflightSkillsSync); -assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSync.plannedActions").copy === false, "compact preflight must show no copy action", preflightSkillsSync); -assertCondition(preflightSkillsSync.valuesPrinted === false, "compact preflight skills sync must declare valuesPrinted=false", preflightSkillsSync); -assertCondition(!JSON.stringify(preflightSkillsSync).includes(forbiddenPathLiteral), "compact preflight must not propagate misspelled path literal"); +assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "full preflight should classify missing skills as runner-skills-blocker", preflightSummary); +assertCondition(preflightSummary.degradedReason === "unapproved-target", "full preflight degraded reason should use the skills sync blocker first", preflightSummary); +assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must show skills target", preflightSkills); +assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "full preflight must show non-mutating skills sync dry-run", preflightSkillsSync); +assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "full preflight must show missing target count", preflightSkillsSync); +assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSync.plannedActions").copy === false, "full preflight must show no copy action", preflightSkillsSync); +assertCondition(preflightSkillsSync.valuesPrinted === false, "full preflight skills sync must declare valuesPrinted=false", preflightSkillsSync); +assertCondition(!JSON.stringify(preflightSkillsSync).includes(forbiddenPathLiteral), "full preflight must not propagate misspelled path literal"); const healthSummary = asRecord(summarizeMicroserviceObservation("health", "code-queue", { ok: true, diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 2fce504f..c369a44d 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -4180,9 +4180,193 @@ function prPreflightAuthScopeSummary(tokenCoverage: Record | nu }; } +function prPreflightTokenCoverage(record: Record): Record | null { + const preflight = asRecord(record.preflight); + const schedulerPreflight = asRecord(record.schedulerPreflight); + const schedulerAuth = asRecord(schedulerPreflight?.auth); + const schedulerSummary = schedulerPreflight === null ? null : { + ok: schedulerPreflight.authReady ?? null, + source: schedulerPreflight.authSource ?? null, + credentialSource: schedulerPreflight.credentialSource ?? null, + scope: schedulerPreflight.scope ?? null, + missing: Array.isArray(schedulerPreflight.missing) && schedulerPreflight.missing.length > 0 + ? schedulerPreflight.missing.map(String) + : schedulerPreflight.authReady === false + ? ["GH_TOKEN", "GITHUB_TOKEN"] + : [], + runnerDisposition: schedulerPreflight.authReady === true ? "ready" : "infra-blocked", + }; + return asRecord(record.tokenCoverage) + ?? asRecord(preflight?.tokenCoverage) + ?? schedulerAuth + ?? schedulerSummary + ?? null; +} + +function prPreflightAuthBroker(record: Record): Record | null { + const preflight = asRecord(record.preflight); + const schedulerPreflight = asRecord(record.schedulerPreflight); + return asRecord(record.authBroker) + ?? asRecord(preflight?.authBroker) + ?? asRecord(schedulerPreflight?.authBroker) + ?? null; +} + +function prPreflightCapabilityContract(record: Record): Record | null { + const preflight = asRecord(record.preflight); + const prCapability = asRecord(record.prCapability); + return asRecord(record.prCapabilityContract) + ?? asRecord(preflight?.prCapabilityContract) + ?? prCapability + ?? null; +} + +function prPreflightCommandSet(record: Record, options: CodexPrPreflightOptions): Record { + const preflight = asRecord(record.preflight); + const commands = asRecord(record.commands) ?? asRecord(preflight?.commands) ?? {}; + const activeRunnerDevContainer = asRecord(record.activeRunnerDevContainer) ?? activeRunnerDevContainerCapability(); + const activeCommands = asRecord(activeRunnerDevContainer.commands) ?? {}; + const remoteFlag = options.remote ? " --remote" : ""; + const fullDetail = `bun scripts/cli.ts codex pr-preflight${remoteFlag} --full`; + return { + verifyActiveRunnerAuth: activeCommands.authStatus ?? commands.local ?? "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk", + verifyActiveRunnerPrCreateDryRun: activeCommands.prCreateDryRun ?? "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title --body-file <file> --base master --head <head> --dry-run", + rerunSchedulerPreflight: commands.runner ?? `bun scripts/cli.ts codex pr-preflight${remoteFlag}`, + fullDetail, + rawProxy: commands.rawProxy ?? "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight?remote=1 --raw --full", + schedulerAuthSource: "configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret", + }; +} + +function compactRecommendedActions(record: Record<string, unknown>): Record<string, unknown>[] { + const actions = Array.isArray(record.recommendedActions) ? record.recommendedActions : []; + if (actions.length > 0) { + return actions.slice(0, 3).map((item) => { + const action = asRecord(item) ?? {}; + return { + scope: action.scope ?? null, + priority: action.priority ?? null, + action: action.action ?? null, + command: action.command ?? null, + writesRemote: action.writesRemote ?? false, + }; + }); + } + const tokenCoverage = prPreflightTokenCoverage(record); + return prPreflightRecommendedActions(tokenCoverage); +} + +function compactPrPreflightCommanderView(record: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> { + if (options.full) return record; + + const tokenCoverage = prPreflightTokenCoverage(record); + const authBroker = prPreflightAuthBroker(record); + const capability = prPreflightCapabilityContract(record); + const activeRunnerDevContainer = asRecord(record.activeRunnerDevContainer) ?? activeRunnerDevContainerCapability(); + const activeCommands = asRecord(activeRunnerDevContainer.commands) ?? {}; + const authBrokerCapability = asRecord(authBroker?.capability); + const expectedPrHandoff = asRecord(capability?.expectedPrHandoff); + const unideskGhCli = asRecord(capability?.unideskGhCli); + const pushDryRun = asRecord(capability?.pushDryRun); + const prCreateDryRun = asRecord(capability?.prCreateDryRun); + const unsupportedMergeBoundary = asRecord(capability?.unsupportedMergeBoundary); + const commands = prPreflightCommandSet(record, options); + const schedulerAuthReady = tokenCoverage?.ok === true; + const activeRunnerTokenCandidatePresent = activeRunnerDevContainer.ok === true; + const failureKind = record.failureKind ?? (schedulerAuthReady ? null : "auth-missing"); + const degradedReason = record.degradedReason ?? (schedulerAuthReady ? null : "auth-broker-needed"); + const githubTransient = asRecord(record.githubTransient); + + return { + ok: record.ok === true, + runnerDisposition: record.runnerDisposition ?? (record.ok === true ? "ready" : "infra-blocked"), + failureKind, + degradedReason, + ...(record.retryable === true ? { retryable: true } : {}), + ...(typeof record.commanderAction === "string" ? { commanderAction: record.commanderAction } : {}), + ...(githubTransient === null ? {} : { githubTransient }), + summary: { + status: record.ok === true ? "ready" : "blocked", + schedulerPreflightAuthReady: schedulerAuthReady, + schedulerPreflightScope: tokenCoverage?.scope ?? "scheduler-runner-env", + activeRunnerTokenCandidatePresent, + activeRunnerScope: activeRunnerDevContainer.scope ?? "current-cli-process", + interpretation: schedulerAuthReady + ? "scheduler preflight auth is ready; still use active-runner dry-runs before writes" + : "scheduler auth is missing for preflight admission only; this does not prove the active runner/dev container cannot create or comment PRs", + }, + schedulerPreflight: { + scope: tokenCoverage?.scope ?? "scheduler-runner-env", + authReady: schedulerAuthReady, + authSource: tokenCoverage?.source ?? null, + credentialSource: tokenCoverage?.credentialSource ?? null, + missing: Array.isArray(tokenCoverage?.missing) ? tokenCoverage.missing.map(String) : [], + failureKind: schedulerAuthReady ? null : failureKind, + degradedReason: schedulerAuthReady ? null : degradedReason, + authBroker: authBroker === null ? null : { + ok: authBroker.ok ?? schedulerAuthReady, + source: authBroker.source ?? null, + configured: authBroker.configured ?? null, + needed: authBroker.needed ?? !schedulerAuthReady, + nextAction: authBroker.nextAction ?? null, + capabilitySource: authBrokerCapability?.source ?? tokenCoverage?.credentialSource ?? null, + }, + }, + activeRunnerPrCapability: { + scope: activeRunnerDevContainer.scope ?? "current-cli-process", + tokenCandidatePresent: activeRunnerTokenCandidatePresent, + credentialSource: activeRunnerDevContainer.credentialSource ?? null, + ghTokenPresent: activeRunnerDevContainer.ghTokenPresent ?? false, + githubTokenPresent: activeRunnerDevContainer.githubTokenPresent ?? false, + independentOfSchedulerPreflight: activeRunnerDevContainer.notEquivalentToSchedulerEnv ?? true, + verifyCommands: { + authStatus: activeCommands.authStatus ?? commands.verifyActiveRunnerAuth, + prCreateDryRun: activeCommands.prCreateDryRun ?? commands.verifyActiveRunnerPrCreateDryRun, + }, + }, + prCapability: capability === null ? null : { + targetBranch: capability.targetBranch ?? "master", + sourceBranch: expectedPrHandoff?.sourceBranch ?? prCreateDryRun?.headBranch ?? null, + unideskGhCliOk: unideskGhCli?.ok ?? null, + systemGhBinaryRequiredForWrites: capability.systemGhBinaryRequiredForWrites ?? false, + realPrCreateRequiresCommanderAuthorization: capability.realPrCreateRequiresCommanderAuthorization ?? true, + pushDryRun: pushDryRun === null ? null : { + requested: pushDryRun.requested ?? false, + writesRemote: pushDryRun.writesRemote ?? false, + commandShape: pushDryRun.commandShape ?? null, + }, + prCreateDryRun: prCreateDryRun === null ? null : { + requested: prCreateDryRun.requested ?? false, + writesRemote: prCreateDryRun.writesRemote ?? false, + commandShape: prCreateDryRun.commandShape ?? null, + }, + mergeSupported: unsupportedMergeBoundary?.supported ?? false, + mergeCommand: unsupportedMergeBoundary?.command ?? "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk", + }, + authScopeSummary: record.authScopeSummary ?? prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer), + scopeBoundary: record.scopeBoundary ?? prPreflightScopeBoundary(tokenCoverage), + recommendedActions: compactRecommendedActions(record), + upstream: record.upstream ?? null, + controlPlane: record.controlPlane ?? null, + observationGap: record.observationGap ?? undefined, + localObservationGap: record.localObservationGap ?? undefined, + localObservationSummary: record.localObservationSummary ?? undefined, + remoteObservationSummary: record.remoteObservationSummary ?? undefined, + commands, + disclosure: { + defaultView: "commander-compact", + fullDetailOmitted: true, + fullObservationsOmitted: asRecord(record.disclosure)?.fullObservationsOmitted ?? true, + expandWith: commands.fullDetail, + rawProxy: commands.rawProxy, + detailFieldsOmitted: ["preflight", "tools", "agentPorts", "git", "egress", "remote", "limitations", "risks", "rawRuntimePreflight"], + }, + }; +} + function decoratePrPreflightScopeBoundary(record: Record<string, unknown>): Record<string, unknown> { const preflight = asRecord(record.preflight); - const tokenCoverage = asRecord(record.tokenCoverage) ?? asRecord(preflight?.tokenCoverage); + const tokenCoverage = prPreflightTokenCoverage(record); const scopeBoundary = prPreflightScopeBoundary(tokenCoverage); const activeRunnerDevContainer = activeRunnerDevContainerCapability(); const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer); @@ -4224,7 +4408,20 @@ function prPreflightObservationGap(kind: Exclude<CodeQueueObservationGapKind, nu function prPreflightObservationSummary(record: Record<string, unknown> | null): Record<string, unknown> | null { if (record === null) return null; const preflight = asRecord(record.preflight); - const tokenCoverage = asRecord(record.tokenCoverage) ?? asRecord(preflight?.tokenCoverage); + const schedulerPreflight = asRecord(record.schedulerPreflight); + const tokenCoverage = asRecord(record.tokenCoverage) + ?? asRecord(preflight?.tokenCoverage) + ?? (schedulerPreflight === null ? null : { + ok: schedulerPreflight.authReady ?? null, + source: schedulerPreflight.authSource ?? null, + credentialSource: schedulerPreflight.credentialSource ?? null, + scope: schedulerPreflight.scope ?? null, + missing: Array.isArray(schedulerPreflight.missing) && schedulerPreflight.missing.length > 0 + ? schedulerPreflight.missing.map(String) + : schedulerPreflight.authReady === false + ? ["GH_TOKEN", "GITHUB_TOKEN"] + : [], + }); const controlPlane = asRecord(record.controlPlane); const targetStack = asRecord(record.targetStack); return { @@ -4561,7 +4758,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP const remoteRecord = asRecord(remoteResponse); if (remoteRecord !== null) { if (remoteRecord.ok === false) { - return decoratePrPreflightScopeBoundary({ + return compactPrPreflightCommanderView(decoratePrPreflightScopeBoundary({ ...remoteRecord, observationGap: prPreflightObservationGap( remoteRecord.failureKind === "control-plane-missing" ? "control-plane-observation-gap" : "runner-local-observation-gap", @@ -4584,9 +4781,9 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP remoteFallbackUsed: true, }, ...maybeFullPrPreflightObservations(options, localRecord, remoteRecord), - }); + }), options); } - return decoratePrPreflightScopeBoundary({ + return compactPrPreflightCommanderView(decoratePrPreflightScopeBoundary({ ...remoteRecord, localObservationGap: prPreflightObservationGap("runner-local-observation-gap", { reason: "local backend-core target-stack absence was bypassed by healthy remote control-plane fallback", @@ -4603,7 +4800,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP remoteFallbackUsed: true, }, ...maybeFullPrPreflightObservations(options, localRecord, remoteRecord), - }); + }), options); } } if (localRecord?.ok !== true) { @@ -4693,13 +4890,14 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP }; } const compact = compactPrRuntimePreflight(preflight, options); - return { + return compactPrPreflightCommanderView({ ok: compact.ok, runnerDisposition: compact.runnerDisposition, failureKind: compact.failureKind ?? null, degradedReason: compact.degradedReason ?? null, ...(compact.retryable === true ? { retryable: true } : {}), ...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}), + ...(asRecord(compact.githubTransient) === null ? {} : { githubTransient: compact.githubTransient }), authScopeSummary: compact.authScopeSummary, scopeBoundary: compact.scopeBoundary, activeRunnerDevContainer: compact.activeRunnerDevContainer, @@ -4711,7 +4909,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP remoteFallbackUsed: false, }, preflight: compact, - }; + }, options); } export function codexPrPreflightQueryForTest(optionArgs: string[], transport: CodeQueuePrPreflightTransport = {}): unknown { @@ -4751,13 +4949,14 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: }; } const compact = compactPrRuntimePreflight(preflight, options); - return { + return compactPrPreflightCommanderView({ ok: compact.ok, runnerDisposition: compact.runnerDisposition, failureKind: compact.failureKind ?? null, degradedReason: compact.degradedReason ?? null, ...(compact.retryable === true ? { retryable: true } : {}), ...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}), + ...(asRecord(compact.githubTransient) === null ? {} : { githubTransient: compact.githubTransient }), authScopeSummary: compact.authScopeSummary, scopeBoundary: compact.scopeBoundary, activeRunnerDevContainer: compact.activeRunnerDevContainer, @@ -4769,7 +4968,7 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: remoteFallbackUsed: false, }, preflight: compact, - }; + }, options); } export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: string): SubmitRoutingRecommendation { diff --git a/scripts/src/help.ts b/scripts/src/help.ts index e73c0616..a791f64f 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -54,7 +54,7 @@ export function rootHelp(): unknown { { 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, while real success only confirms the write and task id." }, { command: "codex skills-sync --dry-run [--full]", description: "Inspect the controlled runner skills hostPath lifecycle contract without copying files, restarting services, reading secrets, or mutating live runner paths." }, - { command: "codex 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]", description: "Read-only PR admission check against the D601 scheduler/runner token, GitHub egress, repo visibility, skills lifecycle health, optional push dry-run, and PR body/create dry-run guard." }, + { command: "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|--raw]", description: "Read-only PR admission check with compact commander output by default; use --full or --raw to expand the full runtime preflight, tool, and observation payload." }, { 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; --detail is still capped, while --full/trace/output explicitly expand evidence." }, { command: "codex tasks [--view supervisor|full] [--queue id] [--status status[,status]] [--unread|--unread-only] [--limit N] [--before-id id]", description: "Show the low-noise supervisor view by default: compact task rows, tiny local sections, activity counts, diagnostics, and drill-down commands; use --view full for detailed rows." }, { 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; default caps large limits/text previews, --full-text explicitly expands one seq window." }, @@ -258,7 +258,7 @@ function codexHelp(): unknown { "bun scripts/cli.ts codex read <taskId>", "bun scripts/cli.ts codex dev-ready", "bun scripts/cli.ts codex skills-sync --dry-run [--full]", - "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]", + "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] [--full|--raw]", "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] [--no-retry|--retry-attempts N]", "bun scripts/cli.ts codex interrupt|cancel <taskId>",