docs: add code queue minimax routing guard
This commit is contained in:
@@ -282,6 +282,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/src/code-queue-liveness-fixtures.ts"),
|
||||
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/src/ci.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
fileItem("scripts/deploy-artifact-matrix-contract-test.ts"),
|
||||
@@ -303,6 +304,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("code-queue:issue3-diagnostics-and-image-preflight", ["bun", "scripts/code-queue-issue3-regression-test.ts"], 30_000));
|
||||
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("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000));
|
||||
items.push(commandItem("code-queue:trace-gap-not-stale", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:trace-gap-not-stale"], 30_000));
|
||||
@@ -320,6 +322,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("code-queue:issue3-diagnostics-and-image-preflight", "Code Queue issue #3 regression fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
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("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
|
||||
@@ -11,6 +11,8 @@ const defaultTextPreviewChars = 12_000;
|
||||
const defaultTasksLimit = 20;
|
||||
const maxTasksLimit = 100;
|
||||
const steerPromptPreviewChars = 320;
|
||||
const minimaxSubmitModel = "minimax-m2.7";
|
||||
const gptSubmitModel = "gpt-5.5";
|
||||
const submitLockWaitMs = 60_000;
|
||||
const submitLockPollMs = 250;
|
||||
const submitLockStaleMs = 120_000;
|
||||
@@ -56,6 +58,43 @@ interface CodexSubmitOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
type SubmitRoute = "minimax-opencode" | "gpt-5.5-codex" | "commander-human-only";
|
||||
type SubmitRouteSignalSeverity = "info" | "warning" | "block";
|
||||
|
||||
interface SubmitRouteSignal {
|
||||
id: string;
|
||||
severity: SubmitRouteSignalSeverity;
|
||||
matched: boolean;
|
||||
evidence: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SubmitRoutingRecommendation {
|
||||
route: SubmitRoute;
|
||||
recommendedRunner: "opencode" | "codex" | "commander";
|
||||
recommendedModel: string | null;
|
||||
confidence: "medium" | "high";
|
||||
reason: string;
|
||||
signals: SubmitRouteSignal[];
|
||||
riskControls: {
|
||||
promptSelfContained: boolean;
|
||||
issueIsNotOnlySource: boolean;
|
||||
noProdRestartSecretOrDbWrite: boolean;
|
||||
evidenceRequiredByPrompt: boolean;
|
||||
commanderMustReviewUnread: true;
|
||||
};
|
||||
explicitRequest: {
|
||||
model: string | null;
|
||||
runner: "opencode" | "codex" | null;
|
||||
note: string | null;
|
||||
};
|
||||
routingPolicy: {
|
||||
dryRunOnly: true;
|
||||
doesNotChangeSubmittedPayload: true;
|
||||
prodMiniMaxAssumedAvailable: false;
|
||||
};
|
||||
}
|
||||
|
||||
interface CodexSteerOptions {
|
||||
prompt: string;
|
||||
dryRun: boolean;
|
||||
@@ -532,6 +571,222 @@ function compactQueuedReason(value: unknown): Record<string, unknown> | null {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSubmitModel(value: string | null | undefined): string {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (raw.length === 0) return raw;
|
||||
const lower = raw.toLowerCase();
|
||||
const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower;
|
||||
if (leaf === minimaxSubmitModel || leaf === "m2.7") return minimaxSubmitModel;
|
||||
return raw;
|
||||
}
|
||||
|
||||
function submitRunnerForModel(model: string | null | undefined): "opencode" | "codex" | null {
|
||||
const normalized = normalizeSubmitModel(model);
|
||||
if (normalized.length === 0) return null;
|
||||
return normalized === minimaxSubmitModel ? "opencode" : "codex";
|
||||
}
|
||||
|
||||
function regexEvidence(text: string, patterns: RegExp[], limit = 6): string[] {
|
||||
const evidence: string[] = [];
|
||||
for (const pattern of patterns) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const value = String(match[0] ?? "").trim().replace(/\s+/gu, " ");
|
||||
if (value.length > 0 && !evidence.includes(value)) evidence.push(value);
|
||||
if (evidence.length >= limit) return evidence;
|
||||
}
|
||||
}
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function regexEvidenceWithoutNegatedContext(text: string, patterns: RegExp[], limit = 6): string[] {
|
||||
const evidence: string[] = [];
|
||||
for (const pattern of patterns) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const index = match.index ?? 0;
|
||||
const context = text.slice(Math.max(0, index - 36), Math.min(text.length, index + String(match[0] ?? "").length + 36));
|
||||
if (/(?:禁止|不要|不得|不能|不应|请勿|严禁|avoid|forbid|forbidden|do not|don't|must not|no\s+)/iu.test(context)) continue;
|
||||
const value = String(match[0] ?? "").trim().replace(/\s+/gu, " ");
|
||||
if (value.length > 0 && !evidence.includes(value)) evidence.push(value);
|
||||
if (evidence.length >= limit) return evidence;
|
||||
}
|
||||
}
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function routeSignal(id: string, severity: SubmitRouteSignalSeverity, evidence: string[], message: string): SubmitRouteSignal {
|
||||
return { id, severity, matched: evidence.length > 0, evidence, message };
|
||||
}
|
||||
|
||||
function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRoutingRecommendation {
|
||||
const prompt = options.prompt;
|
||||
const lower = prompt.toLowerCase();
|
||||
const prodOrStateMutation = regexEvidenceWithoutNegatedContext(lower, [
|
||||
/\bprod(?:uction)?\b/gu,
|
||||
/\brestart(?:ing|ed)?\b/gu,
|
||||
/\brebuild(?:ing|ed)?\b/gu,
|
||||
/\bdeploy(?:ment|ing|ed)?\b/gu,
|
||||
/\bserver\s+rebuild\b/gu,
|
||||
/\bdeploy\s+apply\b/gu,
|
||||
/\binterrupt\b|\bcancel\b/gu,
|
||||
/\bsecret\b|\btoken\b|\bapi[_-]?key\b|\bcredential\b/gu,
|
||||
/\bpostgres(?:ql)?\b|\bpsql\b|\bdatabase\b|\bdb\b|\bsql\b|\bmigration\b/gu,
|
||||
]);
|
||||
const runtimeCore = regexEvidence(lower, [
|
||||
/\bcode[- ]queue\s+(?:runtime|scheduler|backend|execution|runner)\b/gu,
|
||||
/\bbackend-core\b/gu,
|
||||
/\bprovider-gateway\b/gu,
|
||||
/\bk3sctl-adapter\b/gu,
|
||||
/\bruntime-preflight\b/gu,
|
||||
/\bactive\s+run\b/gu,
|
||||
]);
|
||||
const issueReference = regexEvidence(lower, [
|
||||
/\bgithub\s+issue\b/gu,
|
||||
/\bissue\s+#?\d+\b/gu,
|
||||
/#\d+/gu,
|
||||
/\bgh\s+issue\s+view\b/gu,
|
||||
]);
|
||||
const issueOnly = regexEvidence(lower, [
|
||||
/\bread\s+(?:the\s+)?issue\b/gu,
|
||||
/\bsee\s+(?:github\s+)?issue\b/gu,
|
||||
/\bfrom\s+issue\s+#?\d+\b/gu,
|
||||
/\bissue\s+(?:has|contains)\s+(?:the\s+)?(?:full|complete)\s+(?:context|requirements?)\b/gu,
|
||||
/读取\s*(?:github\s*)?issue/gu,
|
||||
/查看\s*(?:github\s*)?issue/gu,
|
||||
]);
|
||||
const issueAuxiliaryGuard = regexEvidence(prompt, [
|
||||
/issue[^。\n]*辅助引用/giu,
|
||||
/issue[^。\n]*不能作为唯一来源/giu,
|
||||
/issue[^。\n]*not[^。\n]*only[^。\n]*source/giu,
|
||||
/issue[^。\n]*auxiliary[^。\n]*reference/giu,
|
||||
]);
|
||||
const lowRiskEvidence = regexEvidence(lower, [
|
||||
/\bdry-run\b/gu,
|
||||
/\bpreflight\b/gu,
|
||||
/\bcontract\s+test\b/gu,
|
||||
/\btypecheck\b/gu,
|
||||
/\bdocs?\b|\bdocumentation\b/gu,
|
||||
/\bread[- ]?only\b/gu,
|
||||
/只读/gu,
|
||||
/文档/gu,
|
||||
/轻量/gu,
|
||||
]);
|
||||
const evidenceRequest = regexEvidence(lower, [
|
||||
/\bevidence\b/gu,
|
||||
/\bverification\b|\bverified\b|\bvalidate\b|\bvalidation\b/gu,
|
||||
/\btest(?:s|ed|ing)?\b/gu,
|
||||
/\bdry-run\b/gu,
|
||||
/\bcommit\b/gu,
|
||||
/验证/gu,
|
||||
/证据/gu,
|
||||
/自测/gu,
|
||||
]);
|
||||
const selfContainedHints = regexEvidence(prompt, [
|
||||
/目标[::]/gu,
|
||||
/范围[::]/gu,
|
||||
/禁止[::]/gu,
|
||||
/验证[::]/gu,
|
||||
/final response/giu,
|
||||
/完整需求/gu,
|
||||
/本 prompt/gu,
|
||||
]);
|
||||
const destructiveWords = regexEvidenceWithoutNegatedContext(lower, [
|
||||
/\brm\s+-rf\b/gu,
|
||||
/\bgit\s+reset\s+--hard\b/gu,
|
||||
/\bgit\s+checkout\s+--\b/gu,
|
||||
/\bdrop\s+table\b/gu,
|
||||
/\btruncate\s+table\b/gu,
|
||||
/\bdelete\s+from\b/gu,
|
||||
]);
|
||||
const crossModule = regexEvidence(lower, [
|
||||
/\bcross[- ]module\b/gu,
|
||||
/\barchitecture\b|\barchitectural\b/gu,
|
||||
/\brelease\/v1\b/gu,
|
||||
/\bci\/cd\b/gu,
|
||||
/\brollout\b|\brollback\b/gu,
|
||||
/跨模块/gu,
|
||||
/架构/gu,
|
||||
/回滚方案|复杂回滚/gu,
|
||||
]);
|
||||
const model = normalizeSubmitModel(options.model);
|
||||
const explicitRunner = submitRunnerForModel(model);
|
||||
const promptSelfContained = prompt.length >= 700 || selfContainedHints.length >= 3;
|
||||
const issueIsNotOnlySource = issueReference.length === 0 || issueAuxiliaryGuard.length > 0 || issueOnly.length === 0 && prompt.length >= 500;
|
||||
const noProdRestartSecretOrDbWrite = prodOrStateMutation.length === 0 && destructiveWords.length === 0;
|
||||
const evidenceRequiredByPrompt = evidenceRequest.length > 0;
|
||||
const signals = [
|
||||
routeSignal("prod-state-secret-db-write", "block", [...prodOrStateMutation, ...destructiveWords], "Mentions production/state mutation, restart, secrets, DB writes, or destructive commands."),
|
||||
routeSignal("runtime-core", "warning", runtimeCore, "Touches Code Queue runtime, backend-core, provider-gateway, k3s adapter, or active run behavior."),
|
||||
routeSignal("issue-source-risk", "warning", issueOnly, "Prompt appears to rely on GitHub issue reading as task context."),
|
||||
routeSignal("issue-auxiliary-source-guard", "info", issueAuxiliaryGuard, "Prompt explicitly says GitHub issue is auxiliary and not the only source."),
|
||||
routeSignal("cross-module-release", "warning", crossModule, "Mentions cross-module architecture, CI/CD rollout, release line, or rollback work."),
|
||||
routeSignal("low-risk-verifiable", "info", lowRiskEvidence, "Mentions low-risk or verifiable work such as docs, read-only checks, dry-run, preflight, or contract tests."),
|
||||
routeSignal("evidence-requested", "info", evidenceRequest, "Prompt asks for tests, validation, commit, or evidence."),
|
||||
routeSignal("self-contained-hints", "info", selfContainedHints, "Prompt includes explicit task sections that make it easier to verify without reading an issue."),
|
||||
];
|
||||
|
||||
let route: SubmitRoute = "gpt-5.5-codex";
|
||||
let recommendedRunner: SubmitRoutingRecommendation["recommendedRunner"] = "codex";
|
||||
let recommendedModel: string | null = gptSubmitModel;
|
||||
let confidence: SubmitRoutingRecommendation["confidence"] = "medium";
|
||||
let reason = "Default to GPT-5.5 when the prompt is not clearly low-risk and self-contained.";
|
||||
|
||||
if (prodOrStateMutation.length > 0 || destructiveWords.length > 0) {
|
||||
route = "commander-human-only";
|
||||
recommendedRunner = "commander";
|
||||
recommendedModel = null;
|
||||
confidence = "high";
|
||||
reason = "This task mentions production/state mutation, restart, secrets, DB writes, or destructive operations; keep it with the commander or a human.";
|
||||
} else if (runtimeCore.length > 0 || crossModule.length > 0) {
|
||||
route = "gpt-5.5-codex";
|
||||
confidence = "high";
|
||||
reason = "This task touches runtime/core/cross-module or release-governance surfaces, so it should stay on GPT-5.5.";
|
||||
} else if (promptSelfContained && issueIsNotOnlySource && evidenceRequiredByPrompt && lowRiskEvidence.length > 0) {
|
||||
route = "minimax-opencode";
|
||||
recommendedRunner = "opencode";
|
||||
recommendedModel = minimaxSubmitModel;
|
||||
confidence = "high";
|
||||
reason = "The prompt looks self-contained, low-risk, and asks for verifiable evidence; it is a MiniMax/OpenCode candidate if the runner smoke is currently green.";
|
||||
} else if (lowRiskEvidence.length > 0 && issueIsNotOnlySource && noProdRestartSecretOrDbWrite) {
|
||||
route = "minimax-opencode";
|
||||
recommendedRunner = "opencode";
|
||||
recommendedModel = minimaxSubmitModel;
|
||||
confidence = "medium";
|
||||
reason = "The prompt has low-risk signals, but the commander should tighten self-contained context and evidence requirements before relying on MiniMax.";
|
||||
}
|
||||
|
||||
const explicitNote = model.length === 0
|
||||
? null
|
||||
: explicitRunner === recommendedRunner
|
||||
? "Explicit --model matches the dry-run recommendation."
|
||||
: "Explicit --model differs from the dry-run recommendation; this dry-run does not rewrite the payload.";
|
||||
|
||||
return {
|
||||
route,
|
||||
recommendedRunner,
|
||||
recommendedModel,
|
||||
confidence,
|
||||
reason,
|
||||
signals,
|
||||
riskControls: {
|
||||
promptSelfContained,
|
||||
issueIsNotOnlySource,
|
||||
noProdRestartSecretOrDbWrite,
|
||||
evidenceRequiredByPrompt,
|
||||
commanderMustReviewUnread: true,
|
||||
},
|
||||
explicitRequest: {
|
||||
model: model.length === 0 ? null : model,
|
||||
runner: explicitRunner,
|
||||
note: explicitNote,
|
||||
},
|
||||
routingPolicy: {
|
||||
dryRunOnly: true,
|
||||
doesNotChangeSubmittedPayload: true,
|
||||
prodMiniMaxAssumedAvailable: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactSchedulerHeartbeat(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
@@ -2149,6 +2404,21 @@ export function codexPrPreflightQueryForTest(optionArgs: string[], fetcher: Code
|
||||
return codeQueuePrPreflight(optionArgs, fetcher);
|
||||
}
|
||||
|
||||
export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: string): SubmitRoutingRecommendation {
|
||||
return submitRoutingRecommendation({
|
||||
prompt,
|
||||
model,
|
||||
queueId: undefined,
|
||||
providerId: undefined,
|
||||
cwd: undefined,
|
||||
reasoningEffort: undefined,
|
||||
executionMode: undefined,
|
||||
maxAttempts: undefined,
|
||||
referenceTaskIds: [],
|
||||
dryRun: true,
|
||||
});
|
||||
}
|
||||
|
||||
function codexSubmitTask(args: string[]): unknown {
|
||||
const options = parseSubmitOptions(args);
|
||||
const payload = submitPayload(options);
|
||||
@@ -2156,10 +2426,16 @@ function codexSubmitTask(args: string[]): unknown {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
routingRecommendation: submitRoutingRecommendation(options),
|
||||
request: {
|
||||
...payload,
|
||||
prompt: textView(options.prompt, true, 3000),
|
||||
},
|
||||
commands: {
|
||||
submitAsRequested: "remove --dry-run to submit exactly this payload",
|
||||
minimaxCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${minimaxSubmitModel} --dry-run`,
|
||||
gptCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${gptSubmitModel} --dry-run`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const locked = runWithSubmitLock(() => unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/tasks"), { method: "POST", body: payload })));
|
||||
|
||||
Reference in New Issue
Block a user