import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { lintCommanderPrompt } from "./src/commander-prompt-lint"; type JsonRecord = Record; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); } function asRecord(value: unknown, label: string): JsonRecord { assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value); return value as JsonRecord; } function asStringArray(value: unknown, label: string): string[] { assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be string array`, value); return value as string[]; } function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; envelope: JsonRecord } { const result = spawnSync("bun", ["scripts/cli.ts", ...args], { cwd: process.cwd(), input: stdin, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, }); assertCondition(String(result.stdout || "").trim().length > 0, `command produced no stdout: ${args.join(" ")}`, { status: result.status, stderr: String(result.stderr || ""), }); return { status: result.status, stdout: String(result.stdout || ""), stderr: String(result.stderr || ""), envelope: asRecord(JSON.parse(String(result.stdout || "")) as unknown, "cli envelope"), }; } function dataOf(envelope: JsonRecord): JsonRecord { return asRecord(envelope.data, "data"); } const completePrompt = ` UniDesk#20 #118 / commander prompt boundary lint You are a D601 Code Queue GPT-5.5 runner. Work in UniDesk only. PR and Git authorization: - You may create and update a head branch and PR, rebase/update from origin/master, resolve conflicts, and self-merge/close the ordinary PR when checks pass and the task boundary is satisfied. Artifact authorization: - You may use repo-owned CI/CD, publish, or equivalent controlled build paths to build/publish DEV images or artifacts, and must report commit, image tag, digest, artifact report, and validation evidence. Rollout boundary: - DEV deploy apply, rollout, and live health verification are owned by the host commander unless this prompt explicitly contains ROLLOUT_OK. - Without explicit ROLLOUT_OK, do not acquire the DEV CD lock, run deploy apply, run rollout restart, or compete with host commander live verification. Forbidden: - No PROD mutation, no reading or printing secrets/tokens/credentials, no manual database/DB writes, and no destructive rollback. `; const incompletePromptWithSecret = ` UniDesk#20 #118 Task: implement the lint. token=ghp_prompt_lint_contract_secret Please make code changes and tests. `; const lint = lintCommanderPrompt(completePrompt); assertCondition(lint.ok === true, "complete GPT-5.5 PR prompt should pass lint", lint); assertCondition(lint.missingClauses.length === 0, "complete prompt should have no missing clauses", lint); assertCondition(lint.suggestedPatchSnippet === "", "passing prompt should not include patch snippet", lint); assertCondition(lint.policy.advisoryOnly === true, "lint must be advisory only", lint); assertCondition(lint.policy.changesCodexSubmitDefault === false, "lint must not change codex submit default", lint); assertCondition(JSON.stringify(lint).includes("promptShape"), "lint should expose prompt shape metadata", lint); assertCondition(!JSON.stringify(lint).includes("self-merge/close the ordinary PR"), "direct lint result must not echo full prompt", lint); const failingLint = lintCommanderPrompt(incompletePromptWithSecret); assertCondition(failingLint.ok === false, "incomplete prompt should fail lint", failingLint); assertCondition(failingLint.riskLevel === "high", "incomplete prompt should be high risk", failingLint); for (const expected of [ "pr-self-merge-rebase-authorization", "artifact-build-publish-authorization", "host-owned-dev-rollout", "runner-rollout-forbidden-without-rollout-ok", "prod-secret-db-rollback-boundary", ]) { assertCondition(failingLint.missingClauses.includes(expected), `missing expected clause id ${expected}`, failingLint); } assertCondition(failingLint.suggestedPatchSnippet.includes("ROLLOUT_OK"), "snippet should mention ROLLOUT_OK", failingLint); assertCondition(!JSON.stringify(failingLint).includes("ghp_prompt_lint_contract_secret"), "lint output must not echo secret-like prompt text", failingLint); const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-prompt-lint-")); try { const promptFile = join(tmp, "prompt.md"); writeFileSync(promptFile, incompletePromptWithSecret, "utf8"); const fileRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--prompt-file", promptFile]); assertCondition(fileRun.status === 0, "commander prompt-lint is advisory and should exit 0 even when lint ok=false", fileRun); assertCondition(fileRun.envelope.ok === true, "CLI envelope should remain ok for advisory lint result", fileRun.envelope); const fileData = dataOf(fileRun.envelope); assertCondition(fileData.ok === false, "lint data ok should reflect missing clauses", fileData); assertCondition(asStringArray(fileData.missingClauses, "missingClauses").includes("host-owned-dev-rollout"), "file lint should report rollout clause", fileData); assertCondition(String(fileData.suggestedPatchSnippet || "").includes("DEV deploy apply"), "file lint should include bounded patch snippet", fileData); assertCondition(!fileRun.stdout.includes("ghp_prompt_lint_contract_secret"), "file lint stdout must not echo prompt secret", fileRun.stdout); const stdinRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--stdin"], completePrompt); assertCondition(stdinRun.status === 0 && stdinRun.envelope.ok === true, "stdin lint should exit successfully", stdinRun.envelope); const stdinData = dataOf(stdinRun.envelope); assertCondition(stdinData.ok === true, "stdin lint data should pass for complete prompt", stdinData); assertCondition(asStringArray(stdinData.missingClauses, "missingClauses").length === 0, "stdin lint should have no missing clauses", stdinData); assertCondition(!stdinRun.stdout.includes("Artifact authorization:"), "stdin lint must not echo full prompt", stdinRun.stdout); } finally { rmSync(tmp, { recursive: true, force: true }); } const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-lint-contract", "--dry-run"], incompletePromptWithSecret); assertCondition(submitDryRun.status === 0 && submitDryRun.envelope.ok === true, "codex submit --dry-run should not be gated by commander prompt-lint", submitDryRun.envelope); const submitData = dataOf(submitDryRun.envelope); assertCondition(asRecord(submitData.request, "submit request").prompt !== undefined, "submit dry-run should keep its existing prompt review behavior", submitData); const helpRun = runCli(["commander", "--help"]); assertCondition(helpRun.status === 0 && helpRun.envelope.ok === true, "commander help should succeed", helpRun.envelope); const helpData = dataOf(helpRun.envelope); assertCondition(asStringArray(helpData.usage, "help usage").some((line) => line.includes("commander prompt-lint")), "commander help should list prompt-lint", helpData); assertCondition(asRecord(helpData.promptLint, "promptLint").gate === "advisory-only; not a business PR gate and not a Code Queue submit admission change", "help should document advisory-only gate", helpData); const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8"); for (const snippet of [ "commander prompt-lint --kind gpt55-pr", "missingClauses", "suggestedPatchSnippet", "不是业务 PR 门禁", ]) { assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`); } process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "complete GPT-5.5 PR prompt passes commander prompt-lint", "missing PR/artifact/DEV rollout/ROLLOUT_OK/PROD-secret-DB-rollback clauses are reported as high risk", "prompt-lint supports --prompt-file and --stdin", "prompt-lint output does not echo full prompt or secret-like prompt text", "commander prompt-lint remains advisory and does not gate codex submit --dry-run", "commander help and host commander reference document the advisory lint entry", ], }, null, 2)}\n`);