From 77b577a2ccfad5631074c4cb28ef783dc6a70457 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sat, 23 May 2026 00:36:45 +0800 Subject: [PATCH] fix(code-queue): harden codex submit prompt input Host commander merge after read-only audit and post-#72 mergeability refresh. PR #71 is CLEAN/MERGEABLE and limited to UniDesk Code Queue CLI prompt input hardening, help/check docs, and submit prompt contract test. It does not touch HWLAB business code. --- docs/reference/cli.md | 2 +- docs/reference/code-queue-supervision.md | 2 +- scripts/cli.ts | 38 +++++- ...e-queue-cli-submit-prompt-contract-test.ts | 123 ++++++++++++++++++ scripts/src/check.ts | 3 + scripts/src/help.ts | 20 ++- 6 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 scripts/code-queue-cli-submit-prompt-contract-test.ts diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cf219331..91a3f969 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -44,7 +44,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core` 与 `publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/:` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId`、`sourceCommit`、`sourceRepo`、`dockerfile`、`imageRef`、`tag`、`digest`、`digestRef`),但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`。 - `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 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`,不能套用任务详情的预览截断策略,否则长任务 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"`。该 `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` 的紧凑行;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` 分页约束。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 0af9801a..c98259c2 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -45,7 +45,7 @@ Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Codex 处理高风险和复杂任务,DeepSeek/OpenCode 处理中等复杂度且边界清晰的任务,MiniMax/OpenCode 处理简单、低权限、可复核任务,生产重启、密钥、数据库手工写入和运行中任务控制保留给指挥官或人工。 -当前提交合同由 `bun scripts/cli.ts codex submit` 暴露:prompt 必须来自位置参数、`--prompt-file` 或 `--prompt-stdin`;可选字段包括 `--queue/--queue-id`、`--provider-id/--provider`、`--cwd/--workdir`、`--model`、`--reasoning-effort`、`--execution-mode/--mode`、`--max-attempts` 和 `--reference-task-id/--reference/--ref`。这些字段写入任务 payload 后由 `code-queue-mgr` 入 PostgreSQL,核心任务字段包括 `queue_id`、`provider_id`、`execution_mode`、`model`、`cwd`、`prompt/base_prompt`、`reference_task_ids`、`reasoning_effort`、`max_attempts` 和 `task_json`;队列记录至少有 `id/name/created_at/updated_at`。模型治理应优先看任务 payload 和数据库字段,不靠 worker final response 自报。 +当前提交合同由 `bun scripts/cli.ts codex submit` 暴露:prompt 必须来自位置参数、`--prompt-file` 或 `--prompt-stdin`;可选字段包括 `--queue/--queue-id`、`--provider-id/--provider`、`--cwd/--workdir`、`--model`、`--reasoning-effort`、`--execution-mode/--mode`、`--max-attempts` 和 `--reference-task-id/--reference/--ref`。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 应使用 `--prompt-stdin` 或 `--prompt-file`,例如 `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`;位置参数只适合短单行 smoke prompt。提交前先用 `--dry-run` 检查完整 payload,确认后移除 `--dry-run`。这些字段写入任务 payload 后由 `code-queue-mgr` 入 PostgreSQL,核心任务字段包括 `queue_id`、`provider_id`、`execution_mode`、`model`、`cwd`、`prompt/base_prompt`、`reference_task_ids`、`reasoning_effort`、`max_attempts` 和 `task_json`;队列记录至少有 `id/name/created_at/updated_at`。模型治理应优先看任务 payload 和数据库字段,不靠 worker final response 自报。 运行态默认模型仍是 `gpt-5.5`。`CODE_QUEUE_MODELS` 当前长期合同至少包含 GPT-5.5、GPT-5.4、GPT-5.4 Mini、DeepSeek Chat 和 MiniMax M2.7;`deepseek`/`deepseek-chat` 与 `minimax-m2.7` 会走 OpenCode port,其余模型走 Codex port。只有当执行面 `/health` 或等价配置已经显示 DeepSeek 模型可用、并完成轻量 runner smoke 后,才允许真实提交 `--model deepseek-chat`。 diff --git a/scripts/cli.ts b/scripts/cli.ts index 67609893..802db025 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -30,11 +30,47 @@ const commandName = displayCommandName(args); function displayCommandName(parts: string[]): string { if (parts.length === 0) return "help"; + if (parts[0] === "codex" && (parts[1] === "submit" || parts[1] === "enqueue")) { + const shown = ["codex", parts[1]]; + const shownValueOptions = new Set([ + "--prompt-file", + "--file", + "--queue", + "--queue-id", + "--provider", + "--provider-id", + "--cwd", + "--workdir", + "--model", + "--reasoning-effort", + "--execution-mode", + "--mode", + "--max-attempts", + "--reference-task-id", + "--reference", + "--ref", + ]); + const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file"); + const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin"); + const hasHelp = parts.slice(2).some(isHelpToken); + if (!hasPromptFile && !hasPromptStdin && !hasHelp) shown.push(""); + for (let index = 2; index < parts.length; index += 1) { + const part = parts[index] ?? ""; + if (!part.startsWith("--")) continue; + shown.push(part); + if (shownValueOptions.has(part)) { + shown.push(parts[index + 1] ?? ""); + index += 1; + } + } + return shown.join(" "); + } if (parts[0] === "codex" && parts[1] === "steer" && parts[2] !== undefined) { const shown = ["codex", "steer", parts[2]]; const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file"); const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin"); - if (!hasPromptFile && !hasPromptStdin) shown.push(""); + const hasHelp = parts.slice(3).some(isHelpToken); + if (!hasPromptFile && !hasPromptStdin && !hasHelp) shown.push(""); for (let index = 3; index < parts.length; index += 1) { const part = parts[index] ?? ""; if (!part.startsWith("--")) continue; diff --git a/scripts/code-queue-cli-submit-prompt-contract-test.ts b/scripts/code-queue-cli-submit-prompt-contract-test.ts new file mode 100644 index 00000000..2823256a --- /dev/null +++ b/scripts/code-queue-cli-submit-prompt-contract-test.ts @@ -0,0 +1,123 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +type JsonRecord = Record; + +function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { + if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); +} + +function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; json: JsonRecord | null } { + const result = spawnSync("bun", ["scripts/cli.ts", ...args], { + cwd: process.cwd(), + input: stdin, + encoding: "utf8", + }); + const stdout = String(result.stdout || ""); + let json: JsonRecord | null = null; + try { + json = JSON.parse(stdout) as JsonRecord; + } catch { + json = null; + } + return { + status: result.status, + stdout, + stderr: String(result.stderr || ""), + json, + }; +} + +function nestedRecord(value: unknown, path: string[]): JsonRecord { + let current: unknown = value; + for (const key of path) { + assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected object while traversing JSON", { path, key, current }); + current = (current as JsonRecord)[key]; + } + assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected nested object", { path, current }); + return current as JsonRecord; +} + +function stringArray(value: unknown): string[] { + return Array.isArray(value) ? value.map((item) => String(item)) : []; +} + +function assertDryRunPrompt(response: JsonRecord, expectedText: string): void { + assertCondition(response.ok === true, "submit dry-run should succeed", response); + const data = nestedRecord(response.data, []); + assertCondition(data.dryRun === true, "submit dry-run should expose dryRun=true", data); + const request = nestedRecord(response.data, ["request"]); + const prompt = nestedRecord(request, ["prompt"]); + assertCondition(prompt.text === expectedText, "submit dry-run prompt text mismatch", prompt); + assertCondition(prompt.chars === expectedText.length, "submit dry-run prompt char count mismatch", prompt); + assertCondition(prompt.truncated === false, "submit dry-run prompt must expose the full prompt", prompt); +} + +export function runCodeQueueCliSubmitPromptContract(): JsonRecord { + const multilinePrompt = [ + "Goal: verify stdin prompt path", + "JSON: {\"quote\":\"'single' and \\\"double\\\"\"}", + "Markdown: `code` | table | value", + "Backslash: C:\\tmp\\prompt", + "", + ].join("\n"); + + const stdin = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-contract", "--dry-run"], multilinePrompt); + assertDryRunPrompt(stdin.json ?? {}, multilinePrompt); + assertCondition(String(stdin.json?.command || "") === "codex submit --prompt-stdin --queue prompt-contract --dry-run", "stdin command should list flags without echoing prompt", stdin.json ?? {}); + + const tmp = mkdtempSync(join(tmpdir(), "unidesk-code-queue-submit-")); + const promptFile = join(tmp, "prompt.md"); + const filePrompt = `${multilinePrompt}file prompt tail\n`; + writeFileSync(promptFile, filePrompt, "utf8"); + try { + const fromFile = runCli(["codex", "submit", "--prompt-file", promptFile, "--queue", "prompt-contract", "--dry-run"]); + assertDryRunPrompt(fromFile.json ?? {}, filePrompt); + assertCondition(String(fromFile.json?.command || "").includes(`--prompt-file ${promptFile}`), "prompt-file command should retain file path for review", fromFile.json ?? {}); + assertCondition(!String(fromFile.json?.command || "").includes("file prompt tail"), "prompt-file command must not echo file prompt text", fromFile.json ?? {}); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + + const positional = runCli(["codex", "submit", "short smoke prompt", "--dry-run"]); + assertDryRunPrompt(positional.json ?? {}, "short smoke prompt"); + assertCondition(String(positional.json?.command || "").includes(""), "outer command should redact positional submit prompt", positional.json ?? {}); + assertCondition(!String(positional.json?.command || "").includes("short smoke prompt"), "outer command must not echo positional submit prompt", positional.json ?? {}); + + const duplicateSource = runCli(["codex", "submit", "positional", "--prompt-stdin", "--dry-run"], "stdin\n"); + assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout }); + const duplicateMessage = String(nestedRecord(duplicateSource.json, ["error"]).message || ""); + assertCondition(duplicateMessage.includes("exactly one prompt source"), "duplicate prompt source error should be explicit", { duplicateMessage }); + + const help = runCli(["codex", "submit", "--help"]); + assertCondition(help.status === 0 && help.json?.ok === true, "codex submit help should succeed", help.json ?? { stdout: help.stdout }); + const data = nestedRecord(help.json?.data, []); + const usage = stringArray(data.usage); + const promptInput = nestedRecord(data, ["promptInput"]); + const recommended = stringArray(promptInput.recommended); + const examples = nestedRecord(data, ["examples"]); + assertCondition(usage.some((line) => line.includes("--prompt-stdin")), "help usage should include --prompt-stdin", { usage }); + assertCondition(usage.some((line) => line.includes("--prompt-file")), "help usage should include --prompt-file", { usage }); + assertCondition(usage.some((line) => line.includes("cat <<'PROMPT'")), "help usage should include a quoted heredoc example", { usage }); + assertCondition(recommended.includes("--prompt-stdin") && recommended.includes("--prompt-file"), "help should recommend stdin and file prompt sources", promptInput); + assertCondition(String(promptInput.sourceRule || "").includes("Exactly one prompt source"), "help should document exact prompt source rule", promptInput); + assertCondition(stringArray(examples.stdin).some((line) => line.includes("--prompt-stdin")), "help examples should include stdin command", examples); + assertCondition(String(examples.file || "").includes("--prompt-file"), "help examples should include file command", examples); + + return { + ok: true, + checks: [ + "submit --prompt-stdin preserves multiline quotes and newlines", + "submit --prompt-file preserves reviewed file contents", + "submit positional prompt is redacted from the outer command envelope", + "duplicate submit prompt source fails explicitly", + "codex submit help documents stdin/file recommendations and copyable examples", + ], + }; +} + +if (import.meta.main) { + process.stdout.write(`${JSON.stringify(runCodeQueueCliSubmitPromptContract(), null, 2)}\n`); +} diff --git a/scripts/src/check.ts b/scripts/src/check.ts index a0cd72b2..b7abc679 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-cli-submit-prompt-contract-test.ts", "scripts/code-queue-supervisor-disclosure-contract-test.ts", "src/components/frontend/src/index.ts", "src/components/frontend/src/app.tsx", @@ -341,6 +342,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:runner-skills-contract", ["bun", "scripts/code-queue-runner-skills-contract-test.ts"], 30_000)); + items.push(commandItem("code-queue:submit-prompt-contract", ["bun", "scripts/code-queue-cli-submit-prompt-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)); @@ -367,6 +369,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:runner-skills-contract", "Code Queue runner skill availability contract is opt-in with script checks", "--scripts-typecheck or --full")); + items.push(skippedItem("code-queue:submit-prompt-contract", "Code Queue submit prompt 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")); diff --git a/scripts/src/help.ts b/scripts/src/help.ts index b90ea29d..cff45612 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -248,7 +248,9 @@ function codexHelp(): unknown { output: "json", usage: [ "bun scripts/cli.ts codex deploy # disabled legacy deployment entry", - "bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue id] [--dry-run]", + "bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue id] [--model model] [--dry-run]", + "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", "bun scripts/cli.ts codex task [--detail] [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", "bun scripts/cli.ts codex tasks [--view supervisor|full] [--queue id] [--status succeeded,running] [--unread|--unread-only] [--limit N] [--before-id id]", "bun scripts/cli.ts codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", @@ -260,6 +262,22 @@ function codexHelp(): unknown { "bun scripts/cli.ts codex interrupt|cancel ", "bun scripts/cli.ts codex queues [--full|--all] | queue create | queue merge --into | move --queue ", ], + promptInput: { + recommended: ["--prompt-stdin", "--prompt-file"], + stdin: "Use a quoted heredoc or pipe when the prompt contains newlines, quotes, backticks, JSON, Markdown tables, or shell-sensitive text.", + file: "Use --prompt-file for reviewed or reusable dispatch prompts; --file is an alias.", + positional: "Keep positional prompt usage to short one-line smoke prompts only.", + sourceRule: "Exactly one prompt source is accepted: positional prompt, --prompt-file, or --prompt-stdin.", + }, + examples: { + stdin: [ + "cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run", + "", + "PROMPT", + ], + file: "bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run", + dryRunThenSubmit: "Run with --dry-run first; remove --dry-run to submit exactly the same payload.", + }, description: "Operate Code Queue through the stable backend-core private proxy path.", }; }