diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 60293f46..6bbd999f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -45,9 +45,9 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run ` 只接受 failed run,从原 run 反查 `scheduleId` 后重触发同一 schedule,并输出 `originalRunId`、`scheduleId`、`newRunId` 和 `observeCommand`。当 backend-core 目标容器缺失或只观察到 verify-only 容器时,schedule/microservice 命令必须以非零退出并返回 `failureKind=target-stack-not-running`、`runnerDisposition=infra-blocked`、`readOnlyCommands` 和 `authorizationRequiredForRecovery`,不得把 Docker 的 `No such container` 当成成功的空历史。 - `codex deploy ` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 - `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。dry-run 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/运行态或 release 禁止/证据要求/中等复杂度候选等 guard 状态;同时输出 `policyContract`,固定暴露 GPT-5.5、DeepSeek、MiniMax 的风险分层、并发上限和外部 provider 429 退避处置。该建议只用于指挥官 preflight,不会改写 payload,不改变 runtime admission,也不假设生产 MiniMax 或 DeepSeek 可用。提交确认和 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/] [--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"`。`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]` 通过稳定 `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 能力。`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 task ` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。需要旧式详细摘要时显式加 `--detail`;需要完整 prompt/response 文本时加 `--full`;需要工具调用、judge、attempt 全量摘要时使用 `--detail --full --tool-limit N`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 -- `codex tasks [--view supervisor|full] [--queue id] [--status succeeded|running|queued|failed|canceled|judging|retry_wait[,..]] [--unread|--unread-only] [--limit N] [--before-id id]` 通过同一私有代理输出渐进式披露视图。默认 `supervisor` 只返回 `running`、`completedUnread`、`recentCompleted`、`queued` 和 `executionDiagnostics` 摘要,不嵌入完整 Trace、final response 或全量 overview;每个条目都带 `commands.show`、`commands.trace`、`commands.output`、`commands.read` 和 `commands.full`。`--unread` 是 `--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败,不能静默忽略。需要完整当前页任务简表时显式使用 `--view full` 或 `--full`,仍受 `--limit` 和 `--before-id` 分页约束。 +- `codex 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` 的紧凑行;prompt/body 只给短预览和原始字符数,`recentCompleted` 默认最多返回 5 条且不重复 `completedUnread` 未读终态,不嵌入完整 Trace、final response 或全量 overview。每个条目只保留 `commands.show/detail/trace/output/full/read` 作为精确展开入口,并带 `classification` 标记直接推进、部署修复、验证/报告噪声等类别,帮助指挥官按 #131 聚焦真实推进而不是被 Gate/报告/审查任务牵引。`--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`。 - `codex output --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。 - `codex read ` 在人工审阅后标记单个终态任务已读;列表、overview 和 supervisor 视图只返回这个命令字段,不得自动执行,也不得批量清空未读状态。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 34f3c494..ecfbff7d 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -151,6 +151,8 @@ 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。 + 该命令经 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 缺失只作为证据,不是最终主阻塞。若远程控制面可达,输出继续保留 ready preflight;若远程控制面不可达,结构化失败归类为 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`。输出中的 `prCapabilityContract` 用于指挥官快速审查 runner handoff:目标分支固定显示、push/PR create dry-run 标记为不写远端、系统 `gh` binary 与 UniDesk REST `bun scripts/cli.ts gh` 可用性分开报告,且 merge 明确保持 `unsupported-command`。 本地 runner preflight 示例: @@ -176,7 +178,7 @@ bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base m 常用入口: -- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认有界监督视图,包括 running、完成未读、最近完成、queued/runnable、execution diagnostics 和下一步 drill-down 命令。 +- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认低噪声监督视图,包括 running、完成未读、最多 5 条最近完成、queued/runnable、execution diagnostics、任务分类和下一步 drill-down 命令。默认行只保留短 prompt/body 预览、原始字符数和 `commands.show/detail/trace/output/full/read`,需要更多内容再按 taskId 展开。 - `bun scripts/cli.ts codex queues`:查看低噪声队列计数、active task id、完成未读队列、runnable 队列和控制面诊断;只有需要完整队列行时才加 `--full`。summary 和 full 都使用稳定 JSON path `.data.queues.items[]` 读取队列行,并从 `.data.queues.counts` 与 `.data.queues.executionDiagnostics` 读取全局计数和执行诊断。 - `bun scripts/cli.ts codex tasks --unread --limit N`:查看完成未读审阅积压;`--unread` 与 `--unread-only` 等价,不能被静默忽略。 - `bun scripts/cli.ts codex tasks --status succeeded --unread --limit N`:按具体终态过滤监督结果;不支持的 status filter 必须显式失败,不能扩大为未过滤结果。 @@ -184,7 +186,9 @@ bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base m - 当默认审阅摘要不足时,再逐级使用 `bun scripts/cli.ts codex task --detail`、`bun scripts/cli.ts codex task --trace --limit N` 或 `codex output`。 - 当 master 控制面状态和 D601 scheduler 状态看起来分裂时,使用 `docs/reference/observability.md` 中的活性规则判断。 -默认 supervisor 视图必须保持低噪声。每个任务应带 `commands.show`、`commands.trace`、`commands.output`、`commands.full` 和 `commands.read`,让下一步渐进披露动作明确;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。 +默认 supervisor 视图必须保持低噪声。每个任务应带 `commands.show`、`commands.detail`、`commands.trace`、`commands.output`、`commands.full` 和 `commands.read`,让下一步渐进披露动作明确;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`recentCompleted` 必须默认限量,且不得重复 `completedUnread` 里的未读终态,避免完成历史把当前 running、阻塞和未读审阅挤出视野;需要完整当前页时显式使用 `--view full`。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。 + +这条规则直接服务 HWLAB #131:指挥官要优先看到真实业务推进、部署修复、阻塞和需要人工审阅的未读结果,Gate/报告/审查/诊断类任务只能作为折叠的分类信号存在,不能在默认输出中用长 prompt/body 抢占上下文。 完成未读任务的审阅也必须遵循渐进披露。指挥官默认只拉取原始 prompt 和最终 response,用它判断任务是否声称完成、是否有明显越界、是否缺少验收证据;不要默认拉完整 trace、全量 tool summary 或 raw output。只有当 final response 与目标不一致、证据不足、远端 commit 无法验证、任务疑似造假、或需要追溯失败原因时,才继续展开 `--detail`、分页 `--trace`、或按 seq 读取 `codex output`。这条规则的目标是降低上下文压力,同时保留通过多步查询拿到完整证据的能力。 diff --git a/scripts/code-queue-pr-preflight-contract-test.ts b/scripts/code-queue-pr-preflight-contract-test.ts index b5cc67b6..b2901994 100644 --- a/scripts/code-queue-pr-preflight-contract-test.ts +++ b/scripts/code-queue-pr-preflight-contract-test.ts @@ -1,4 +1,5 @@ import { codexPrPreflightQueryForTest } from "./src/code-queue"; +import type { UniDeskConfig } from "./src/config"; type JsonRecord = Record; @@ -354,37 +355,46 @@ async function main(): Promise { assertCondition(remoteControlPlaneMissingRecord.degradedReason === "remote-control-plane-unreachable", "missing control plane should classify as remote-control-plane-unreachable", remoteControlPlaneMissingRecord); assertCondition(asRecord(remoteControlPlaneMissingRecord.controlPlane).localBackendCoreMissing === true, "local backend-core absence should remain evidence only", remoteControlPlaneMissingRecord.controlPlane); - const directAuthMissing = remoteControlPlaneResult({ - ok: false, - failureKind: "auth-missing", - degradedReason: "GH_TOKEN/GITHUB_TOKEN missing", - runnerDisposition: "infra-blocked", - message: "GH_TOKEN/GITHUB_TOKEN missing in remote control plane", - tokenCoverage: { + const directAuthMissing = await codexPrPreflightQueryForTest(["--remote"], { + config: { network: { publicHost: "74.48.78.17", frontend: { port: 18081 } } } as unknown as UniDeskConfig, + coreFetch: () => localBackendCoreMissingFixture(), + remoteMainServerPrPreflight: () => remoteControlPlaneResult({ ok: false, - source: null, - ghTokenPresent: false, - githubTokenPresent: false, - ghCredentialStorePresent: false, + failureKind: "auth-missing", + degradedReason: "GH_TOKEN/GITHUB_TOKEN missing", runnerDisposition: "infra-blocked", - missing: ["GH_TOKEN", "GITHUB_TOKEN"], - scope: "scheduler-runner-env", - }, - prCapabilityContract: { - targetBranch: "master", - tokenSource: null, - systemGhBinaryRequiredForWrites: false, - unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true, role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh", requiresSystemGhBinary: false }, - pushDryRun: { requested: false, ref: "refs/heads/probe/code-queue-pr-capability-dryrun", writesRemote: false, commandShape: "git push --dry-run origin HEAD:refs/heads/probe/code-queue-pr-capability-dryrun" }, - prCreateDryRun: { requested: false, headBranch: "feature/code-queue-pr-preflight", writesRemote: false, commandShape: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --base master --head feature/code-queue-pr-preflight --dry-run" }, - expectedPrHandoff: { sourceBranch: "feature/code-queue-pr-preflight", targetBranch: "master", runnerCreatesPrAfterAuthorization: true, commanderReviewsAndMerges: true, preflightCreatesPr: false, preflightMergesPr: false }, - unsupportedMergeBoundary: { supported: false, command: "bun scripts/cli.ts gh pr merge --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." }, - }, + message: "GH_TOKEN/GITHUB_TOKEN missing in remote control plane", + tokenCoverage: { + ok: false, + source: null, + ghTokenPresent: false, + githubTokenPresent: false, + ghCredentialStorePresent: false, + runnerDisposition: "infra-blocked", + missing: ["GH_TOKEN", "GITHUB_TOKEN"], + scope: "scheduler-runner-env", + }, + prCapabilityContract: { + targetBranch: "master", + tokenSource: null, + systemGhBinaryRequiredForWrites: false, + unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true, role: "repo-native REST GitHub CLI used by bun scripts/cli.ts gh", requiresSystemGhBinary: false }, + pushDryRun: { requested: false, ref: "refs/heads/probe/code-queue-pr-capability-dryrun", writesRemote: false, commandShape: "git push --dry-run origin HEAD:refs/heads/probe/code-queue-pr-capability-dryrun" }, + prCreateDryRun: { requested: false, headBranch: "feature/code-queue-pr-preflight", writesRemote: false, commandShape: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --base master --head feature/code-queue-pr-preflight --dry-run" }, + expectedPrHandoff: { sourceBranch: "feature/code-queue-pr-preflight", targetBranch: "master", runnerCreatesPrAfterAuthorization: true, commanderReviewsAndMerges: true, preflightCreatesPr: false, preflightMergesPr: false }, + unsupportedMergeBoundary: { supported: false, command: "bun scripts/cli.ts gh pr merge --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." }, + }, + }), }); const directAuthMissingRecord = asRecord(directAuthMissing); assertCondition(directAuthMissingRecord.ok === false, "auth-missing remote result should fail", directAuthMissingRecord); assertCondition(directAuthMissingRecord.failureKind === "auth-missing", "missing token should classify as auth-missing", directAuthMissingRecord); assertCondition(directAuthMissingRecord.degradedReason === "GH_TOKEN/GITHUB_TOKEN missing", "auth missing should state token gap", directAuthMissingRecord); + const directAuthScopeBoundary = asRecord(directAuthMissingRecord.scopeBoundary); + const directAuthActiveRunner = asRecord(directAuthMissingRecord.activeRunnerDevContainer); + assertCondition(directAuthScopeBoundary.scopesAreIndependent === true, "remote auth-missing must distinguish scheduler env from active runner dev container", 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); const gitRemoteGap = remoteControlPlaneResult({ ok: false, @@ -575,8 +585,12 @@ async function main(): Promise { 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(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(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); @@ -713,11 +727,15 @@ async function main(): Promise { assertCondition(missingTokenRecord.degradedReason === "auth-broker-needed", "missing-token branch should expose broker-needed degraded reason", missingTokenRecord); const missingTokenPreflight = asRecord(missingTokenRecord.preflight); const missingTokenAuthBroker = asRecord(missingTokenPreflight.authBroker); + const missingTokenScopeBoundary = asRecord(missingTokenPreflight.scopeBoundary); + const missingTokenActiveRunner = asRecord(missingTokenPreflight.activeRunnerDevContainer); const missingTokenCapability = asRecord(missingTokenAuthBroker.capability); 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(String(missingTokenScopeBoundary.currentRunnerCheck ?? "").includes("gh auth status"), "missing-token branch should point to active runner auth check", 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); process.stdout.write(`${JSON.stringify({ ok: true, diff --git a/scripts/code-queue-supervisor-disclosure-contract-test.ts b/scripts/code-queue-supervisor-disclosure-contract-test.ts new file mode 100644 index 00000000..1e4e2484 --- /dev/null +++ b/scripts/code-queue-supervisor-disclosure-contract-test.ts @@ -0,0 +1,164 @@ +import { codexTasksQueryForTest } from "./src/code-queue"; + +type JsonRecord = Record; + +function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void { + if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); +} + +function asRecord(value: unknown): JsonRecord { + assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), "expected JSON object", { value }); + return value as JsonRecord; +} + +function asArray(value: unknown): unknown[] { + assertCondition(Array.isArray(value), "expected JSON array", { value }); + return value as unknown[]; +} + +function longText(marker: string, repeat: number): string { + return Array.from({ length: repeat }, (_, index) => `${marker}-${index} #132 Gate report diagnostic review evidence direct workbench fix`).join("\n"); +} + +function task(id: string, status: string, updatedAt: string, readAt: string | null = null): JsonRecord { + return { + id, + queueId: "default", + status, + currentAttempt: status === "running" ? 2 : 1, + updatedAt, + finishedAt: status === "succeeded" ? updatedAt : null, + readAt, + prompt: longText(`prompt-${id}`, 90), + basePrompt: longText(`base-${id}`, 70), + displayPrompt: longText(`display-${id}`, 80), + lastAssistantMessage: { + at: updatedAt, + seq: 99, + source: "assistant", + text: longText(`assistant-${id}`, 120), + }, + }; +} + +function fixtureResponse(path: string): JsonRecord { + if (path.includes("/summary")) { + const taskId = decodeURIComponent(path.split("/api/tasks/")[1]?.split("/")[0] ?? "unknown"); + return { + ok: true, + status: 200, + body: { + ok: true, + summary: { + id: taskId, + queueId: "default", + status: taskId.includes("running") ? "running" : "succeeded", + currentAttempt: 1, + maxAttempts: 99, + prompt: longText(`summary-prompt-${taskId}`, 100), + basePrompt: longText(`summary-base-${taskId}`, 80), + lastAssistantMessage: { + at: "2026-05-22T00:00:00.000Z", + seq: 120, + source: "assistant", + text: longText(`summary-assistant-${taskId}`, 130), + }, + commands: { + show: `bun scripts/cli.ts codex task ${taskId}`, + trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit 80`, + }, + }, + }, + }; + } + assertCondition(path.startsWith("/api/microservices/code-queue/proxy/api/tasks/overview"), "unexpected path", { path }); + return { + ok: true, + status: 200, + body: { + ok: true, + queue: { + executionDiagnostics: { + effectiveLiveness: "live", + splitBrainLive: true, + recommendedAction: "continue-supervision", + }, + }, + pagination: { + limit: 200, + returned: 15, + total: 15, + hasMore: false, + nextBeforeId: null, + includeActive: true, + }, + tasks: [ + task("task-running", "running", "2026-05-22T00:09:00.000Z"), + task("task-succeeded-1", "succeeded", "2026-05-22T00:08:00.000Z"), + task("task-succeeded-2", "succeeded", "2026-05-22T00:07:00.000Z"), + task("task-succeeded-3", "succeeded", "2026-05-22T00:06:00.000Z"), + task("task-succeeded-4", "succeeded", "2026-05-22T00:05:00.000Z"), + task("task-succeeded-5", "succeeded", "2026-05-22T00:04:00.000Z"), + task("task-succeeded-6", "succeeded", "2026-05-22T00:03:00.000Z"), + task("task-succeeded-7", "succeeded", "2026-05-22T00:02:00.000Z"), + task("task-read-1", "succeeded", "2026-05-22T00:01:50.000Z", "2026-05-22T00:01:55.000Z"), + task("task-read-2", "succeeded", "2026-05-22T00:01:40.000Z", "2026-05-22T00:01:45.000Z"), + task("task-read-3", "succeeded", "2026-05-22T00:01:30.000Z", "2026-05-22T00:01:35.000Z"), + task("task-read-4", "succeeded", "2026-05-22T00:01:20.000Z", "2026-05-22T00:01:25.000Z"), + task("task-read-5", "succeeded", "2026-05-22T00:01:10.000Z", "2026-05-22T00:01:15.000Z"), + task("task-read-6", "succeeded", "2026-05-22T00:01:05.000Z", "2026-05-22T00:01:09.000Z"), + task("task-queued", "queued", "2026-05-22T00:01:00.000Z"), + ], + }, + }; +} + +export function runCodeQueueSupervisorDisclosureContract(): JsonRecord { + const supervisor = codexTasksQueryForTest(["--view", "supervisor", "--limit", "20"], fixtureResponse); + const full = codexTasksQueryForTest(["--view", "full", "--limit", "20"], fixtureResponse); + + const supervisorBody = JSON.stringify(supervisor); + const fullBody = JSON.stringify(full); + const supervisorData = asRecord(supervisor); + const supervisorView = asRecord(supervisorData.supervisor); + const disclosure = asRecord(supervisorView.disclosure); + const runningItem = asRecord(asArray(asRecord(supervisorView.running).items)[0]); + const recentCompleted = asRecord(supervisorView.recentCompleted); + const recentItems = asArray(recentCompleted.items); + const prompt = asRecord(runningItem.prompt); + const lastMessage = asRecord(runningItem.lastMessage); + const commands = asRecord(runningItem.commands); + const fullItem = asRecord(asArray(asRecord(asRecord(full).tasks).items)[0]); + const completedUnread = asRecord(supervisorView.completedUnread); + const fullTasks = asRecord(asRecord(full).tasks); + + assertCondition(supervisorBody.length < fullBody.length * 0.55, "supervisor output should be materially smaller than full output", { supervisorChars: supervisorBody.length, fullChars: fullBody.length }); + assertCondition(recentItems.length === 5, "recentCompleted should be capped below --limit by default", { returned: recentItems.length }); + assertCondition(asArray(completedUnread.items).length === 7, "completedUnread should keep unread terminal tasks separate from recentCompleted", completedUnread); + assertCondition(recentItems.every((item) => asRecord(item).unreadTerminal === false), "recentCompleted should not duplicate unread terminal tasks", { recentItems }); + assertCondition(asArray(runningItem.issueRefs).includes("#132"), "supervisor row should expose issue refs for triage", runningItem); + assertCondition(Number(prompt.chars) > String(prompt.text ?? "").length && prompt.truncated === true, "supervisor prompt must be a short preview with original char count", prompt); + assertCondition(Number(lastMessage.chars) > String(lastMessage.text ?? "").length && lastMessage.truncated === true, "supervisor body must be a short preview with original char count", lastMessage); + assertCondition(commands.show !== undefined && commands.trace !== undefined && commands.output !== undefined && commands.full !== undefined, "supervisor row must keep progressive drill-down commands", commands); + assertCondition(runningItem.promptPreview === undefined && runningItem.lastAssistantMessage === undefined, "supervisor rows must not expose legacy long list fields", runningItem); + assertCondition(asRecord(fullItem.promptPreview).chars !== undefined && fullItem.lastAssistantMessage !== undefined, "full view must retain detailed task row fields", fullItem); + assertCondition(fullTasks.returned === 15, "full view must not inherit supervisor recentCompleted cap", fullTasks); + assertCondition(asRecord(disclosure.outputBudget).recentCompletedReturnedLimit === 5, "supervisor must expose output budget metadata", disclosure); + + return { + ok: true, + checks: [ + "supervisor output materially smaller than full", + "recentCompleted capped", + "prompt/body previews bounded", + "drill-down commands preserved", + "full view remains detailed", + ], + supervisorChars: supervisorBody.length, + fullChars: fullBody.length, + }; +} + +if (import.meta.main) { + process.stdout.write(`${JSON.stringify(runCodeQueueSupervisorDisclosureContract(), null, 2)}\n`); +} diff --git a/scripts/src/check.ts b/scripts/src/check.ts index b000f2e8..5fe461c1 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -30,6 +30,7 @@ const syntaxFiles = [ "scripts/host-codex-commander-no-daemon-smoke-contract-test.ts", "scripts/host-codex-commander-skeleton-contract-test.ts", "scripts/auth-broker-contract-test.ts", + "scripts/code-queue-supervisor-disclosure-contract-test.ts", "src/components/frontend/src/index.ts", "src/components/frontend/src/app.tsx", "src/components/frontend/src/decision-center.tsx", @@ -303,6 +304,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default fileItem("scripts/code-queue-trace-summary-contract-test.ts"), fileItem("scripts/code-queue-pr-preflight-contract-test.ts"), fileItem("scripts/code-queue-submit-routing-contract-test.ts"), + fileItem("scripts/code-queue-supervisor-disclosure-contract-test.ts"), fileItem("scripts/host-codex-commander-skeleton-contract-test.ts"), fileItem("scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"), fileItem("scripts/provider-runner-triage-contract-test.ts"), @@ -338,6 +340,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default items.push(commandItem("code-queue:trace-summary-contract", ["bun", "scripts/code-queue-trace-summary-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:pr-preflight-contract", ["bun", "scripts/code-queue-pr-preflight-contract-test.ts"], 30_000)); items.push(commandItem("code-queue:submit-routing-contract", ["bun", "scripts/code-queue-submit-routing-contract-test.ts"], 30_000)); + items.push(commandItem("code-queue:supervisor-disclosure-contract", ["bun", "scripts/code-queue-supervisor-disclosure-contract-test.ts"], 30_000)); items.push(commandItem("host-codex-commander:skeleton-contract", ["bun", "scripts/host-codex-commander-skeleton-contract-test.ts"], 30_000)); items.push(commandItem("host-codex-commander:no-daemon-smoke-contract", ["bun", "scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"], 30_000)); items.push(commandItem("provider:runner-triage-contract", ["bun", "scripts/provider-runner-triage-contract-test.ts"], 30_000)); @@ -362,6 +365,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:submit-routing-contract", "Code Queue submit routing contract is opt-in with script checks", "--scripts-typecheck or --full")); + items.push(skippedItem("code-queue:supervisor-disclosure-contract", "Code Queue supervisor disclosure contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("host-codex-commander:skeleton-contract", "host Codex commander skeleton contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("host-codex-commander:no-daemon-smoke-contract", "host Codex commander no-daemon smoke contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("provider:runner-triage-contract", "Provider runner triage contract is opt-in with script checks", "--scripts-typecheck or --full")); diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 82391f51..6f6b9191 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -12,6 +12,10 @@ const defaultOutputLimit = 20; const defaultTextPreviewChars = 12_000; const defaultTasksLimit = 20; const maxTasksLimit = 100; +const supervisorRecentCompletedLimit = 5; +const supervisorPromptPreviewChars = 160; +const supervisorBodyPreviewChars = 180; +const supervisorRecentBodyPreviewChars = 80; const steerPromptPreviewChars = 320; const minimaxSubmitModel = "minimax-m2.7"; const deepseekSubmitModel = "deepseek-chat"; @@ -187,7 +191,35 @@ interface CodexTasksEntry { }; } -interface CodexTasksSection { +interface CodexTasksSupervisorEntry { + taskId: string; + queueId: string | null; + status: string | null; + currentAttempt: number | null; + updatedAt: string | null; + finishedAt: string | null; + unreadTerminal: boolean; + issueRefs: string[]; + classification: { + kind: "direct-progress" | "deployment-fix" | "verification" | "management-noise" | "documentation" | "unknown"; + labels: string[]; + managementNoise: boolean; + reason: string; + }; + prompt: Record; + lastMessage: Record | null; + queuedReason: Record | null; + commands: { + show: string; + detail: string; + trace: string; + output: string; + full: string; + read: string; + }; +} + +interface CodexTasksSection { count: number; returned: number; truncated: boolean; @@ -195,8 +227,11 @@ interface CodexTasksSection { commands: { next: string | null; full: string; + detailTemplate?: string; + traceTemplate?: string; + outputTemplate?: string; }; - items: CodexTasksEntry[]; + items: T[]; } interface CodexTasksDegraded { @@ -362,6 +397,10 @@ function textPreview(value: string, maxChars: number): Record { }; } +function compactInlinePreview(value: string, maxChars: number): Record { + return textPreview(value.replace(/\s+/gu, " ").trim(), maxChars); +} + function fmtDuration(ms: unknown): string { const value = Number(ms); if (!Number.isFinite(value) || value < 0) return "--"; @@ -1572,6 +1611,75 @@ function taskQueuedRunnable(task: Record): boolean { return asRecord(task.queuedReason)?.code === "ready"; } +function taskIssueRefs(task: Record, summary: Record | null): string[] { + const text = [ + asString(task.displayPrompt), + asString(task.basePrompt), + asString(task.prompt), + asString(summary?.initialPrompt), + asString(summary?.basePrompt), + asString(summary?.prompt), + asString(summary?.lastError), + asString(task.lastError), + ].join("\n"); + return Array.from(new Set(Array.from(text.matchAll(/#(\d{1,6})\b/gu)).map((match) => `#${match[1]}`))).slice(0, 8); +} + +function taskClassification(task: Record, summary: Record | null): CodexTasksSupervisorEntry["classification"] { + const text = [ + asString(task.displayPrompt), + asString(task.basePrompt), + asString(task.prompt), + asString(task.lastError), + asString(summary?.lastError), + asString(asRecord(summary?.lastAssistantMessage)?.text), + ].join("\n").toLowerCase(); + const matches = (pattern: RegExp): boolean => pattern.test(text); + const labels: string[] = []; + if (matches(/\b(?:gate|report|aggregator|runbook|contract|audit|review|brief|evidence|diagnostic|observability|visibility|preflight|smoke)\b|报告|审查|汇总|观测|诊断|预检|门禁/iu)) labels.push("management-or-verification"); + if (matches(/\b(?:deploy|deployment|prod|dev|release|artifact|ci|cd)\b|部署|发布|上线/iu)) labels.push("deployment"); + if (matches(/\b(?:fix|bug|repair|implement|feature|ui|frontend|backend|api|database|db|workbench|patch-panel|box-simu|gateway-simu)\b|修复|实现|用户|工作台|接线|仿真|数据库/iu)) labels.push("direct-work"); + if (matches(/\b(?:doc|docs|reference|markdown)\b|文档|参考/iu)) labels.push("documentation"); + if (labels.length === 0) labels.push("uncategorized"); + + if (labels.includes("management-or-verification") && !labels.includes("direct-work") && !labels.includes("deployment")) { + return { kind: "management-noise", labels, managementNoise: true, reason: "matched report/gate/review/diagnostic terms without direct implementation or deployment terms" }; + } + if (labels.includes("deployment")) { + return { kind: "deployment-fix", labels, managementNoise: labels.includes("management-or-verification") && !labels.includes("direct-work"), reason: "matched deployment or artifact terms" }; + } + if (labels.includes("direct-work")) { + return { kind: "direct-progress", labels, managementNoise: false, reason: "matched implementation, user-visible, runtime, or repair terms" }; + } + if (labels.includes("management-or-verification")) { + return { kind: "verification", labels, managementNoise: true, reason: "matched verification/report terms; keep folded unless it blocks real work" }; + } + if (labels.includes("documentation")) { + return { kind: "documentation", labels, managementNoise: false, reason: "matched documentation terms" }; + } + return { kind: "unknown", labels, managementNoise: false, reason: "no strong classifier term matched" }; +} + +function supervisorLastMessage(summaryLastAssistant: unknown, maxChars: number): Record | null { + if (summaryLastAssistant === undefined || summaryLastAssistant === null) return null; + const record = asRecord(summaryLastAssistant) ?? {}; + const text = asString(record.text); + if (text.length === 0) { + return { + at: record.at ?? null, + seq: record.seq ?? null, + source: record.source ?? "none", + ...compactInlinePreview("", maxChars), + }; + } + return { + at: record.at ?? null, + seq: record.seq ?? null, + source: record.source ?? "none", + ...compactInlinePreview(text, maxChars), + }; +} + function taskWatchEntry(task: Record, summary: Record | null): CodexTasksEntry { const taskId = asString(task.id); const summaryCommands = summary === null ? null : asRecord(summary.commands); @@ -1610,6 +1718,40 @@ function taskWatchEntry(task: Record, summary: Record, summary: Record | null, bodyPreviewChars = supervisorBodyPreviewChars): CodexTasksSupervisorEntry { + const taskId = asString(task.id); + const summaryCommands = summary === null ? null : asRecord(summary.commands); + const summaryLastAssistant = summary?.lastAssistantMessage ?? task.lastAssistantMessage; + const showCommand = typeof summary?.cliHint === "string" && summary.cliHint.length > 0 + ? summary.cliHint + : `bun scripts/cli.ts codex task ${taskId}`; + const traceCommand = typeof summary?.traceHint === "string" && summary.traceHint.length > 0 + ? summary.traceHint + : `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`; + return { + taskId, + queueId: asString(task.queueId) || null, + status: asString(task.status) || null, + currentAttempt: typeof task.currentAttempt === "number" && Number.isFinite(task.currentAttempt) ? task.currentAttempt : null, + updatedAt: asString(task.updatedAt) || null, + finishedAt: asString(task.finishedAt) || null, + unreadTerminal: taskUnreadTerminal(task), + issueRefs: taskIssueRefs(task, summary), + classification: taskClassification(task, summary), + prompt: compactInlinePreview(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), supervisorPromptPreviewChars), + lastMessage: supervisorLastMessage(summaryLastAssistant, bodyPreviewChars), + queuedReason: compactQueuedReason(task.queuedReason), + commands: { + show: typeof summaryCommands?.show === "string" && summaryCommands.show.length > 0 ? summaryCommands.show : showCommand, + detail: `bun scripts/cli.ts codex task ${taskId} --detail`, + trace: typeof summaryCommands?.trace === "string" && summaryCommands.trace.length > 0 ? summaryCommands.trace : traceCommand, + output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`, + full: `bun scripts/cli.ts codex task ${taskId} --full`, + read: `bun scripts/cli.ts codex read ${taskId}`, + }, + }; +} + function buildTaskWatchSection( tasks: Record[], summaries: Map>, @@ -1633,6 +1775,33 @@ function buildTaskWatchSection( }; } +function buildSupervisorTaskSection( + tasks: Record[], + summaries: Map>, + limit: number, + nextCommand: string | null, + fullCommand: string, + bodyPreviewChars = supervisorBodyPreviewChars, +): CodexTasksSection { + const visibleTasks = tasks.slice(0, limit); + const items = visibleTasks.map((task) => taskSupervisorEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null, bodyPreviewChars)); + const truncated = tasks.length > limit; + return { + count: tasks.length, + returned: items.length, + truncated, + hasMore: truncated, + commands: { + next: truncated ? nextCommand : null, + full: fullCommand, + detailTemplate: "bun scripts/cli.ts codex task --detail", + traceTemplate: `bun scripts/cli.ts codex task --trace --tail --limit ${defaultTraceLimit}`, + outputTemplate: `bun scripts/cli.ts codex output --tail --limit ${defaultOutputLimit}`, + }, + items, + }; +} + function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>, omittedTaskCount = 0): CodexTasksDegraded | null { if (summaryErrors.length === 0 && omittedTaskCount === 0) return null; return { @@ -1784,18 +1953,26 @@ function codexTasksOverviewResult( const allTasks = filterTasksForOptions(taskPage.tasks, options); const runningTasks = sortRunningWatchTasks(allTasks); const unreadCompletedTasks = sortCompletedWatchTasks(allTasks).filter((task) => taskUnreadTerminal(task)); - const recentCompletedTasks = options.unreadOnly ? [] : sortCompletedWatchTasks(allTasks); + const recentCompletedTasks = options.unreadOnly ? [] : sortCompletedWatchTasks(allTasks).filter((task) => !taskUnreadTerminal(task)); const queuedTasks = options.unreadOnly ? [] : sortQueuedWatchTasks(allTasks); const nextBeforeId = asString(taskPage.pagination.nextBeforeId) || null; const sourceHasMore = asBoolean(taskPage.pagination.hasMore); const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null; const fullCommand = taskListCommand({ ...options, view: "full" }); - const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit, nextCommand, fullCommand); - const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand); - const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit, nextCommand, fullCommand); - const queuedSection = buildTaskWatchSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand); + const recentLimit = Math.min(options.limit, supervisorRecentCompletedLimit); + const runningSection = buildSupervisorTaskSection(runningTasks, summaries, options.limit, nextCommand, fullCommand); + const unreadSection = buildSupervisorTaskSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand); + const recentSection = buildSupervisorTaskSection(recentCompletedTasks, summaries, recentLimit, nextCommand, fullCommand, supervisorRecentBodyPreviewChars); + const queuedSection = buildSupervisorTaskSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand); const pagination = taskPage.pagination; const diagnostics = compactExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics); + const visibleSupervisorItems = [...runningSection.items, ...unreadSection.items, ...recentSection.items, ...queuedSection.items]; + const classifierCounts = visibleSupervisorItems.reduce((counts, item) => { + const key = item.classification.kind; + counts[key] = (counts[key] ?? 0) + 1; + if (item.classification.managementNoise) counts.managementNoise = (counts.managementNoise ?? 0) + 1; + return counts; + }, {} as Record); return { upstream, supervisor: { @@ -1820,6 +1997,13 @@ function codexTasksOverviewResult( bounded: true, disclosure: { defaultView: "supervisor", + policy: "bounded summary rows only; prompt/body are short previews and raw detail requires explicit task/full/trace/output commands", + outputBudget: { + promptPreviewChars: supervisorPromptPreviewChars, + bodyPreviewChars: supervisorBodyPreviewChars, + recentCompletedReturnedLimit: recentLimit, + recentCompletedBodyPreviewChars: supervisorRecentBodyPreviewChars, + }, fullCommand, next: nextCommand, rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`, @@ -1831,6 +2015,7 @@ function codexTasksOverviewResult( recentCompleted: recentSection.count, queued: queuedSection.count, }, + classifierCounts, executionDiagnostics: diagnostics, degraded, commands: { @@ -1901,7 +2086,7 @@ function visibleTaskIdsForOverview(tasks: Record[], options: Co return Array.from(new Set([ ...sortRunningWatchTasks(filtered).slice(0, options.limit), ...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit), - ...sortCompletedWatchTasks(filtered).slice(0, options.limit), + ...sortCompletedWatchTasks(filtered).filter((task) => !taskUnreadTerminal(task)).slice(0, Math.min(options.limit, supervisorRecentCompletedLimit)), ...sortQueuedWatchTasks(filtered).slice(0, options.limit), ].map((task) => taskOverviewCandidateKey(task)))) .filter((taskId) => taskId.length > 0); @@ -1915,6 +2100,10 @@ function codexTasksQuery(taskArgs: string[], fetcher: CodexResponseFetcher = cor return codexTasksOverviewResult(page, upstream, options, summaries, degraded); } +export function codexTasksQueryForTest(taskArgs: string[], fetcher: CodexResponseFetcher): unknown { + return codexTasksQuery(taskArgs, fetcher); +} + async function codexTasksQueryAsync(taskArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise { const options = parseTasksOptions(taskArgs); const byId = new Map>(); @@ -2566,6 +2755,57 @@ function authBrokerNeededStatus(tokenCoverage: Record, runtimeA }; } +function activeRunnerDevContainerCapability(): Record { + const ghTokenPresent = typeof process.env.GH_TOKEN === "string" && process.env.GH_TOKEN.length > 0; + const githubTokenPresent = typeof process.env.GITHUB_TOKEN === "string" && process.env.GITHUB_TOKEN.length > 0; + const anyToken = ghTokenPresent || githubTokenPresent; + return { + scope: "current-cli-process", + applicableWhen: "this command is running inside the active Code Queue runner/dev container", + observed: true, + ok: anyToken, + ghTokenPresent, + githubTokenPresent, + credentialSource: ghTokenPresent ? "GH_TOKEN" : githubTokenPresent ? "GITHUB_TOKEN" : null, + notEquivalentToSchedulerEnv: true, + relationToRemotePreflight: "independent-scope; scheduler-runner-env auth-missing does not prove the active runner/dev container lacks GitHub PR capability", + valuesPrinted: false, + commands: { + authStatus: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk", + prCreateDryRun: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title --body-file <file> --base master --head <head> --dry-run", + prCommentDryRun: "bun scripts/cli.ts gh pr comment create <number> --repo pikasTech/unidesk --body-file <file> --dry-run", + }, + interpretation: anyToken + ? "current CLI process has a GitHub token candidate; validate with gh auth status and PR dry-run before declaring this runner unable to create/comment PRs" + : "current CLI process did not expose GH_TOKEN/GITHUB_TOKEN; this still does not prove another active runner/dev container lacks token coverage", + }; +} + +function prPreflightScopeBoundary(tokenCoverage: Record<string, unknown> | null): Record<string, unknown> { + const schedulerScope = typeof tokenCoverage?.scope === "string" ? tokenCoverage.scope : "scheduler-runner-env"; + return { + schedulerPreflightScope: schedulerScope, + activeRunnerDevContainerScope: "current-cli-process", + scopesAreIndependent: 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", + valuesPrinted: false, + }; +} + +function decoratePrPreflightScopeBoundary(record: Record<string, unknown>): Record<string, unknown> { + const preflight = asRecord(record.preflight); + const tokenCoverage = asRecord(preflight?.tokenCoverage ?? record.tokenCoverage); + const scopeBoundary = prPreflightScopeBoundary(tokenCoverage); + const activeRunnerDevContainer = activeRunnerDevContainerCapability(); + return { + ...record, + scopeBoundary, + activeRunnerDevContainer, + ...(preflight === null ? {} : { preflight: { ...preflight, scopeBoundary } }), + }; +} + function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> { const pull = asRecord(preflight.pullRequestDelivery) ?? {}; const tools = asRecord(pull.tools) ?? {}; @@ -2615,6 +2855,7 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: }, tokenCoverage, authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli), + scopeBoundary: prPreflightScopeBoundary(tokenCoverage), prCapabilityContract: { targetBranch, tokenSource: tokenCoverage.source, @@ -2711,11 +2952,12 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: limitations, risks, runnerDisposition: ok ? "ready" : "infra-blocked", + activeRunnerDevContainer: activeRunnerDevContainerCapability(), 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." - : "Configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the Code Queue scheduler runtime secret for the target queue, then rerun this preflight before creating a PR.", + : "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.", commands: { local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk", runner: "bun scripts/cli.ts codex pr-preflight --remote", @@ -2815,7 +3057,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP const remoteRecord = asRecord(remoteResponse); if (remoteRecord !== null) { if (remoteRecord.ok === false) { - return { + return decoratePrPreflightScopeBoundary({ ...remoteRecord, controlPlane: { ...(asRecord(remoteRecord.controlPlane) ?? {}), @@ -2827,9 +3069,9 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP }, localObservation: localRecord, remoteObservation: remoteRecord, - }; + }); } - return { + return decoratePrPreflightScopeBoundary({ ...remoteRecord, controlPlane: { ...(asRecord(remoteRecord.controlPlane) ?? {}), @@ -2841,7 +3083,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP }, localObservation: localRecord, remoteObservation: remoteRecord, - }; + }); } } if (localRecord?.ok !== true) { diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 8cc5d140..b90ea29d 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -55,7 +55,7 @@ export function rootHelp(): unknown { { command: "codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." }, { command: "codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--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, optional push dry-run, and PR body/create dry-run guard." }, { command: "codex task <taskId> [--detail] [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch the bounded review view by default: original prompt, final response, and drill-down commands; detail and trace are opt-in." }, - { command: "codex tasks [--view supervisor|full] [--queue id] [--status status[,status]] [--unread|--unread-only] [--limit N] [--before-id id]", description: "Show the bounded supervisor view by default: running, unread terminal, recent completed, queued, diagnostics, and drill-down commands." }, + { command: "codex tasks [--view supervisor|full] [--queue id] [--status status[,status]] [--unread|--unread-only] [--limit N] [--before-id id]", description: "Show the low-noise supervisor view by default: compact task rows, capped recent completions, 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 by seq when a trace row has omitted command/output text." }, { command: "codex read <taskId>", description: "Mark one reviewed terminal task read; never run automatically as part of listing." }, { command: "codex dev-ready", description: "Fetch execution-container readiness, including sanitized skill injection status from /api/dev-ready." },