diff --git a/docs/reference/auth-broker.md b/docs/reference/auth-broker.md index 726f5b9a..8491cc51 100644 --- a/docs/reference/auth-broker.md +++ b/docs/reference/auth-broker.md @@ -9,7 +9,7 @@ - `scripts/src/gh.ts`:UniDesk `gh` CLI 已经是 repo-native GitHub REST wrapper,优先读取 `GH_TOKEN`,其次读取 `GITHUB_TOKEN`,再 fallback 到系统 `gh auth token`。它不会打印 token 值,并已把 `missing-token`、`permission-denied`、`scope-insufficient`、`network-proxy-failed`、`unsupported-command` 等失败结构化。 - `scripts/code-queue-pr-preflight-example.ts`:本地示例先检查 env token,再跑 `gh auth status`、`gh pr create --dry-run` 和 `gh pr comment --dry-run`。没有 env token 时该 workflow 直接失败。 - `src/components/microservices/code-queue/src/runtime-preflight.ts`:D601 scheduler preflight 检查 `GH_TOKEN` / `GITHUB_TOKEN` 是否存在,用 curl/gh/git 探测 GitHub API、issue、PR、SSH 和可选 push dry-run。 -- `scripts/src/code-queue.ts`:`codex pr-preflight` 把 runtime preflight 压缩为 runner 合同;`tokenCoverageStatus` 目前要求 env token,缺失时输出 `failureKind=auth-missing`、`degradedReason=GH_TOKEN/GITHUB_TOKEN missing` 和 `runnerDisposition=infra-blocked`。 +- `scripts/src/code-queue.ts`:`codex pr-preflight` 把 runtime preflight 压缩为 runner 合同;scheduler env token 与 active runner/dev container token 是独立 scope。缺少 scheduler env token 或 auth-broker 时输出 `failureKind=auth-missing`、`degradedReason=auth-broker-needed`、`runnerDisposition=infra-blocked`,并用 `authScopeSummary`/`scopeBoundary`/`recommendedActions` 明确这不等于当前 active runner 不能创建 PR;active task 内仍需用 repo-native `bun scripts/cli.ts gh auth status` 和 PR create dry-run 验证。 - `src/components/microservices/code-queue/src/index.ts`:`CODE_QUEUE_REMOTE_CODEX_ENV_KEYS` 默认包含 `GH_TOKEN`、`GITHUB_TOKEN`、`GH_HOST`、`GITHUB_API_URL` 和 `GH_REPO`,用于把 scheduler env 继续传给 provider dev container。这是凭证分发路径,不适合作为 P0 的唯一解。 - `src/components/microservices/code-queue/docker-compose.d601.yml`:D601 Code Queue 从 `.state/code-queue-d601.env` 读取运行时环境,并配置 provider-gateway egress proxy。该文件不能承载本任务中的真实 secret 值。 - `src/components/microservices/code-queue/Dockerfile`:runner 镜像已经包含 `git`、`gh`、`curl`、`jq`、`rg`、`cargo`、`rustc` 和 `rustfmt`,足够执行轻量 broker client、preflight 和 contract test;本 P0 不需要为了调研临时安装新系统包。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 4a5770d0..08008a80 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -161,7 +161,7 @@ Runner preflight 优先使用执行面诊断入口: bun scripts/cli.ts codex pr-preflight --remote --issue 20 ``` -`codex pr-preflight --remote` 的 `auth-missing` 只表示 scheduler/runtime preflight surface(`scheduler-runner-env`)没有看到 `GH_TOKEN/GITHUB_TOKEN` 或 auth-broker,不得被简化成“当前 active runner/dev container 不能创建 PR”。Code Queue 输出必须同时给出 `scopeBoundary` 和 `activeRunnerDevContainer`:前者说明 scheduler env 与当前 CLI/dev container 是独立 scope,后者只报告当前 CLI 进程是否看见 token,且不打印 token 值。指挥官看到 remote preflight `auth-missing` 时,应继续用当前 runner 内的 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk`、`gh pr create --dry-run`、`gh pr comment create --dry-run` 验证实际 PR 能力;只有这些 active runner 检查也失败时,才能把它判成当前 turn 不能 PR。 +`codex pr-preflight --remote` 的 `auth-missing` 只表示 scheduler/runtime preflight surface(`scheduler-runner-env`)没有看到 `GH_TOKEN/GITHUB_TOKEN` 或 auth-broker,不得被简化成“当前 active runner/dev container 不能创建 PR”。默认输出必须优先给出有界的 `authScopeSummary`、`scopeBoundary`、`activeRunnerDevContainer` 和 `recommendedActions`:`authScopeSummary` 用一句话说明 scheduler auth missing 只是 scoped finding;`scopeBoundary` 明确 scheduler env 与当前 CLI/dev container 是独立 scope;`activeRunnerDevContainer` 只报告当前 CLI 进程是否看见 token,且不打印 token 值;`recommendedActions` 保持低噪声,先给 active task 内的 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk` 和 `gh pr create --dry-run`,再给 scheduler 侧长期的 auth-broker 或 runtime secret 修复。指挥官看到 remote preflight `auth-missing` 时,应继续用当前 runner 内的 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk`、`gh pr create --dry-run`、`gh pr comment create --dry-run` 验证实际 PR 能力;只有这些 active runner 检查也失败时,才能把它判成当前 turn 不能 PR。 该命令经 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 `;该 guard 只执行 dry-run,不创建 PR。缺少 env token 时必须返回 `ok=false`、`runnerDisposition=infra-blocked`、`tokenCoverage.missing=["GH_TOKEN","GITHUB_TOKEN"]` 和 `authBroker.source="broker/auth-broker-needed"`,因为 provider dev container 只能转发 scheduler 已经拥有的 token,除非后续接入 broker-held GitHub credential。系统 `gh` binary 缺失只能作为 `tools.systemGhBinary.ok=false` 观测,不得把它误判为 UniDesk REST `bun scripts/cli.ts gh` 不可用。`--remote` 在 runner-like 环境里不再要求本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些本地 target stack 缺失只作为证据,不是最终主阻塞,并应额外标成 `blockingDisposition=runner-local-observation-gap` 或 `localObservationGap.kind=runner-local-observation-gap`。若远程控制面可达,输出继续保留 ready preflight;若远程控制面不可达,结构化失败归类为 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,并额外标成 `blockingDisposition=control-plane-observation-gap`。`runnerDisposition` 可以为了旧调用方兼容继续保持 `infra-blocked`,但 observation-gap 字段才是判断“观测路径缺口,不是 scheduler 停摆”的稳定口径。输出中的 `prCapabilityContract` 用于指挥官快速审查 runner handoff:目标分支固定显示、push/PR create dry-run 标记为不写远端、系统 `gh` binary 与 UniDesk REST `bun scripts/cli.ts gh` 可用性分开报告,且 merge 明确保持 `unsupported-command`。 diff --git a/scripts/code-queue-pr-preflight-contract-test.ts b/scripts/code-queue-pr-preflight-contract-test.ts index 515dc08a..45f0a8f3 100644 --- a/scripts/code-queue-pr-preflight-contract-test.ts +++ b/scripts/code-queue-pr-preflight-contract-test.ts @@ -342,6 +342,11 @@ async function main(): Promise { const fallbackLocalGap = asRecord(fallback.localObservationGap); assertCondition(fallbackLocalGap.kind === "runner-local-observation-gap", "healthy remote fallback should classify local backend-core absence as runner-local observation gap", fallbackLocalGap); assertCondition(fallbackLocalGap.schedulerStoppage === false, "local observation gap must not imply scheduler stoppage", fallbackLocalGap); + assertCondition(fallback.localObservation === undefined, "healthy remote fallback default output should omit full local observation", fallback); + assertCondition(fallback.remoteObservation === undefined, "healthy remote fallback default output should omit full remote observation", fallback); + 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); @@ -399,11 +404,45 @@ async function main(): Promise { assertCondition(directAuthMissingRecord.degradedReason === "GH_TOKEN/GITHUB_TOKEN missing", "auth missing should state token gap", directAuthMissingRecord); const directAuthObservationGap = asRecord(directAuthMissingRecord.observationGap); 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 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(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(asRecord(directAuthMissingRecord.disclosure).fullObservationsOmitted === true, "auth-missing remote fallback should disclose omitted full observations", 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); + + const directAuthMissingFull = await codexPrPreflightQueryForTest(["--remote", "--full"], { + config: { network: { publicHost: "74.48.78.17", frontend: { port: 18081 } } } as unknown as UniDeskConfig, + coreFetch: () => localBackendCoreMissingFixture(), + remoteMainServerPrPreflight: () => remoteControlPlaneResult({ + ok: false, + failureKind: "auth-missing", + degradedReason: "GH_TOKEN/GITHUB_TOKEN missing", + runnerDisposition: "infra-blocked", + tokenCoverage: { + ok: false, + source: null, + missing: ["GH_TOKEN", "GITHUB_TOKEN"], + scope: "scheduler-runner-env", + }, + }), + }); + const directAuthMissingFullRecord = asRecord(directAuthMissingFull); + assertCondition(directAuthMissingFullRecord.localObservation !== undefined, "--full should retain full local observation", directAuthMissingFullRecord); + assertCondition(directAuthMissingFullRecord.remoteObservation !== undefined, "--full should retain full remote observation", directAuthMissingFullRecord); const gitRemoteGap = remoteControlPlaneResult({ ok: false, @@ -747,17 +786,30 @@ async function main(): Promise { assertCondition(missingTokenRecord.ok === false, "missing-token branch should fail", missingTokenRecord); assertCondition(missingTokenRecord.failureKind === "auth-missing", "missing-token branch should classify auth-missing", missingTokenRecord); assertCondition(missingTokenRecord.degradedReason === "auth-broker-needed", "missing-token branch should expose broker-needed degraded reason", missingTokenRecord); + 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(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(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(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); process.stdout.write(`${JSON.stringify({ ok: true, diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 6e04e402..b7f43a48 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -2897,25 +2897,74 @@ function activeRunnerDevContainerCapability(): Record { function prPreflightScopeBoundary(tokenCoverage: Record | null): Record { const schedulerScope = typeof tokenCoverage?.scope === "string" ? tokenCoverage.scope : "scheduler-runner-env"; return { + headline: "scheduler-runner-env auth-missing is not active runner/dev container PR incapability", schedulerPreflightScope: schedulerScope, activeRunnerDevContainerScope: "current-cli-process", scopesAreIndependent: true, + schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr: true, authMissingInterpretation: "auth-missing from codex pr-preflight --remote is scoped to the scheduler/runtime preflight surface; do not simplify it to 'the active runner cannot create PRs'", - currentRunnerCheck: "use activeRunnerDevContainer plus bun scripts/cli.ts gh auth status --repo pikasTech/unidesk for the current dev container", + currentRunnerCheck: "use activeRunnerDevContainer plus bun scripts/cli.ts gh auth status --repo pikasTech/unidesk and gh pr create --dry-run for the current dev container", + valuesPrinted: false, + }; +} + +function prPreflightRecommendedActions(tokenCoverage: Record | null): Record[] { + const schedulerReady = tokenCoverage?.ok === true; + return [ + { + scope: "active-runner-dev-container", + priority: "first", + action: "verify-current-runner-auth", + command: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk", + writesRemote: false, + }, + { + scope: "active-runner-dev-container", + priority: "first", + action: "verify-current-runner-pr-create-plan", + command: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title --body-file <file> --base master --head <head> --dry-run", + writesRemote: false, + }, + { + scope: "scheduler-runtime", + priority: schedulerReady ? "long-term" : "required-for-scheduler-preflight", + action: "configure-scheduler-auth-source", + command: "configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN via the scheduler runtime secret", + writesRemote: false, + }, + ]; +} + +function prPreflightAuthScopeSummary(tokenCoverage: Record<string, unknown> | null, activeRunnerDevContainer: Record<string, unknown>): Record<string, unknown> { + const schedulerReady = tokenCoverage?.ok === true; + return { + schedulerPreflightScope: typeof tokenCoverage?.scope === "string" ? tokenCoverage.scope : "scheduler-runner-env", + schedulerAuthReady: schedulerReady, + schedulerAuthSource: tokenCoverage?.source ?? null, + schedulerAuthMissingIsScoped: !schedulerReady, + activeRunnerDevContainerScope: activeRunnerDevContainer.scope ?? "current-cli-process", + activeRunnerTokenCandidatePresent: activeRunnerDevContainer.ok === true, + interpretation: schedulerReady + ? "scheduler preflight has GitHub auth coverage; active runner PR creation still needs repo-native dry-run verification before real writes" + : "scheduler-runner-env auth-missing does not prove the active runner/dev container lacks PR create/comment capability", valuesPrinted: false, }; } function decoratePrPreflightScopeBoundary(record: Record<string, unknown>): Record<string, unknown> { const preflight = asRecord(record.preflight); - const tokenCoverage = asRecord(preflight?.tokenCoverage ?? record.tokenCoverage); + const tokenCoverage = asRecord(record.tokenCoverage) ?? asRecord(preflight?.tokenCoverage); const scopeBoundary = prPreflightScopeBoundary(tokenCoverage); const activeRunnerDevContainer = activeRunnerDevContainerCapability(); + const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer); + const recommendedActions = prPreflightRecommendedActions(tokenCoverage); return { ...record, + authScopeSummary, scopeBoundary, activeRunnerDevContainer, - ...(preflight === null ? {} : { preflight: { ...preflight, scopeBoundary } }), + recommendedActions, + ...(preflight === null ? {} : { preflight: { ...preflight, authScopeSummary, scopeBoundary, recommendedActions } }), }; } @@ -2943,6 +2992,53 @@ 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 controlPlane = asRecord(record.controlPlane); + const targetStack = asRecord(record.targetStack); + return { + ok: record.ok ?? null, + runnerDisposition: record.runnerDisposition ?? null, + failureKind: record.failureKind ?? null, + degradedReason: record.degradedReason ?? null, + controlPlane: controlPlane === null ? null : { + mode: controlPlane.mode ?? null, + localBackendCoreMissing: controlPlane.localBackendCoreMissing ?? null, + remoteFallbackUsed: controlPlane.remoteFallbackUsed ?? null, + }, + targetStack: targetStack === null ? null : { + missingContainers: Array.isArray(targetStack.missingContainers) ? targetStack.missingContainers.map(String) : [], + verifyOnlyObserved: targetStack.verifyOnlyObserved ?? false, + }, + tokenCoverage: tokenCoverage === null ? null : { + ok: tokenCoverage.ok ?? null, + source: tokenCoverage.source ?? null, + credentialSource: tokenCoverage.credentialSource ?? null, + scope: tokenCoverage.scope ?? null, + missing: Array.isArray(tokenCoverage.missing) ? tokenCoverage.missing.map(String) : [], + }, + }; +} + +function maybeFullPrPreflightObservations(options: CodexPrPreflightOptions, localRecord: Record<string, unknown>, remoteRecord: Record<string, unknown>): Record<string, unknown> { + if (options.full) { + return { + localObservation: localRecord, + remoteObservation: remoteRecord, + }; + } + return { + localObservationSummary: prPreflightObservationSummary(localRecord), + remoteObservationSummary: prPreflightObservationSummary(remoteRecord), + disclosure: { + fullObservationsOmitted: true, + expandWith: "bun scripts/cli.ts codex pr-preflight --remote --full", + }, + }; +} + function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> { const pull = asRecord(preflight.pullRequestDelivery) ?? {}; const tools = asRecord(pull.tools) ?? {}; @@ -2978,6 +3074,10 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: 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 scopeBoundary = prPreflightScopeBoundary(tokenCoverage); + const activeRunnerDevContainer = activeRunnerDevContainerCapability(); + const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer); + const recommendedActions = prPreflightRecommendedActions(tokenCoverage); const result: Record<string, unknown> = { ok, failureKind, @@ -2992,7 +3092,9 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: }, tokenCoverage, authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli), - scopeBoundary: prPreflightScopeBoundary(tokenCoverage), + authScopeSummary, + scopeBoundary, + recommendedActions, prCapabilityContract: { targetBranch, tokenSource: tokenCoverage.source, @@ -3089,12 +3191,12 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: limitations, risks, runnerDisposition: ok ? "ready" : "infra-blocked", - activeRunnerDevContainer: activeRunnerDevContainerCapability(), + activeRunnerDevContainer, recoveryHint: ok ? tokenCoverage.source === "auth-broker" ? "Runner PR workflow has auth-broker coverage for GitHub REST preflight; real PR creation still requires commander authorization." : "Runner PR workflow has env-token coverage for the scheduler." - : "Scheduler preflight lacks GitHub auth coverage. Configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret for scheduler-scoped admission; separately check activeRunnerDevContainer and gh auth status before declaring the current runner unable to create/comment PRs.", + : "Scheduler preflight lacks GitHub auth coverage for scheduler-scoped admission only. First verify the active task with repo-native gh auth status and PR create dry-run; long-term configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret.", commands: { local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk", runner: "bun scripts/cli.ts codex pr-preflight --remote", @@ -3230,8 +3332,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP localBackendCoreMissing: true, remoteFallbackUsed: true, }, - localObservation: localRecord, - remoteObservation: remoteRecord, + ...maybeFullPrPreflightObservations(options, localRecord, remoteRecord), }); } return decoratePrPreflightScopeBoundary({ @@ -3250,8 +3351,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP localBackendCoreMissing: true, remoteFallbackUsed: true, }, - localObservation: localRecord, - remoteObservation: remoteRecord, + ...maybeFullPrPreflightObservations(options, localRecord, remoteRecord), }); } } @@ -3347,6 +3447,10 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP runnerDisposition: compact.runnerDisposition, failureKind: compact.failureKind ?? null, degradedReason: compact.degradedReason ?? null, + authScopeSummary: compact.authScopeSummary, + scopeBoundary: compact.scopeBoundary, + activeRunnerDevContainer: compact.activeRunnerDevContainer, + recommendedActions: compact.recommendedActions, upstream: { ok: response.ok, status: response.status }, controlPlane: { mode: "local-backend-core", @@ -3399,6 +3503,10 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: runnerDisposition: compact.runnerDisposition, failureKind: compact.failureKind ?? null, degradedReason: compact.degradedReason ?? null, + authScopeSummary: compact.authScopeSummary, + scopeBoundary: compact.scopeBoundary, + activeRunnerDevContainer: compact.activeRunnerDevContainer, + recommendedActions: compact.recommendedActions, upstream: { ok: response.ok, status: response.status }, controlPlane: { mode: "remote-frontend",