docs: add next stage cicd dispatch matrix
This commit is contained in:
@@ -19,6 +19,7 @@ import { runSwapCommand } from "./src/swap";
|
||||
import { runDevEnvCommand } from "./src/dev-env";
|
||||
import { runArtifactRegistryCommand } from "./src/artifact-registry";
|
||||
import { runGhCommand } from "./src/gh";
|
||||
import { runCommanderCommand } from "./src/commander";
|
||||
import { isHelpToken, rootHelp, serverHelp, sshHelp, staticNamespaceHelp } from "./src/help";
|
||||
|
||||
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
|
||||
@@ -177,6 +178,14 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "commander") {
|
||||
const result = runCommanderCommand(args.slice(1));
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
|
||||
if (top === "ssh") {
|
||||
|
||||
@@ -26,6 +26,14 @@ const runtimePrompt = `
|
||||
验证:需要证明 scheduler heartbeat、active run、OpenCode session recovery 都正确。
|
||||
`;
|
||||
|
||||
const mediumPrompt = `
|
||||
目标:实现一个前端 React 控制台组件的小功能,给 Code Queue 任务列表增加可折叠的验证证据摘要。
|
||||
范围:只改用户界面模块中的一个 TSX 组件和一个相邻的轻量 contract guard,不触碰 backend-core、Code Queue runtime、provider-gateway、k3sctl-adapter、部署配置或数据库 schema。
|
||||
禁止:不要部署 prod,不要重启服务,不要读取密钥,不要写数据库,不要修改 release/v1,不要跑 heavy check/e2e/Playwright。
|
||||
验证:运行针对该组件或 contract guard 的轻量脚本,final response 必须报告修改文件、验证命令、输出摘要、commit 和遗留风险。
|
||||
背景:本 prompt 是完整需求来源,GitHub issue 只能作为辅助引用,不能作为唯一来源。这个任务有真实代码变更和 UI 状态判断,复杂度高于只读文档,但写入边界局部、可审阅、可用轻量测试复核。
|
||||
`;
|
||||
|
||||
const commanderOnlyPrompt = `
|
||||
目标:在 production 上 deploy apply 并 restart code-queue,必要时读取 secret token 和写 PostgreSQL 修复任务状态。
|
||||
验证:live health。
|
||||
@@ -44,6 +52,18 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord {
|
||||
assertCondition(runtime.recommendedRunner === "codex", "runtime/core work should recommend Codex runner", runtime);
|
||||
assertCondition(runtime.recommendedModel === "gpt-5.5", "runtime/core work should recommend GPT-5.5", runtime);
|
||||
|
||||
const medium = codexSubmitRoutingRecommendationForTest(mediumPrompt, "deepseek");
|
||||
assertCondition(medium.route === "deepseek-codex", "medium bounded frontend work should recommend DeepSeek", medium);
|
||||
assertCondition(medium.recommendedRunner === "codex", "DeepSeek work should use Codex runner", medium);
|
||||
assertCondition(medium.recommendedModel === "deepseek-chat", "DeepSeek candidate should recommend deepseek-chat", medium);
|
||||
assertCondition(asRecord(medium.riskControls).mediumComplexityCandidate === true, "medium prompt should satisfy medium complexity controls", medium);
|
||||
assertCondition(asRecord(medium.explicitRequest).model === "deepseek-chat", "explicit deepseek alias should normalize to deepseek-chat", medium);
|
||||
const policyContract = asRecord(medium.policyContract);
|
||||
assertCondition(asRecord(policyContract.concurrency).gpt55Routine === 5, "policy contract should expose GPT-5.5 routine concurrency", policyContract);
|
||||
assertCondition(asRecord(policyContract.concurrency).gpt55BurstMax === 10, "policy contract should expose GPT-5.5 burst concurrency", policyContract);
|
||||
assertCondition(asRecord(policyContract.concurrency).minimaxSimpleMax === 10, "policy contract should expose MiniMax simple concurrency", policyContract);
|
||||
assertCondition(asRecord(policyContract.concurrency).deepseekMediumDefault === 5, "policy contract should expose DeepSeek medium default concurrency", policyContract);
|
||||
|
||||
const commanderOnly = codexSubmitRoutingRecommendationForTest(commanderOnlyPrompt);
|
||||
assertCondition(commanderOnly.route === "commander-human-only", "prod restart/secrets/DB work should be commander-only", commanderOnly);
|
||||
assertCondition(commanderOnly.recommendedRunner === "commander", "commander-only work should not recommend a runner", commanderOnly);
|
||||
@@ -60,6 +80,8 @@ export function runCodeQueueSubmitRoutingContract(): JsonRecord {
|
||||
checks: [
|
||||
"low-risk self-contained prompts recommend minimax-m2.7/OpenCode",
|
||||
"runtime/core work recommends GPT-5.5/Codex",
|
||||
"medium bounded frontend work recommends deepseek-chat/Codex",
|
||||
"dry-run policy contract exposes model-tier concurrency",
|
||||
"prod/restart/secret/DB work is commander-only",
|
||||
"explicit --model mismatch is visible and payload is unchanged",
|
||||
],
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
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 a string array`, value);
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
function runCli(args: string[], expectStatus: number): JsonRecord {
|
||||
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
assertCondition(result.status === expectStatus, `status mismatch for ${args.join(" ")}`, {
|
||||
status: result.status,
|
||||
stdout: result.stdout.slice(-2000),
|
||||
stderr: result.stderr.slice(-2000),
|
||||
});
|
||||
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
|
||||
}
|
||||
|
||||
function dataOf(envelope: JsonRecord): JsonRecord {
|
||||
return asRecord(envelope.data, "data");
|
||||
}
|
||||
|
||||
const contract = dataOf(runCli(["commander", "contract"], 0));
|
||||
assertCondition(contract.phase === "source-contract", "contract must identify source-contract phase", contract);
|
||||
assertCondition(contract.serviceId === "host-codex-commander", "contract must expose service id", contract);
|
||||
assertCondition(contract.daemonImplemented === false, "daemon must not be implemented in phase one", contract);
|
||||
assertCondition(contract.liveOperationsImplemented === false, "live operations must not be implemented in phase one", contract);
|
||||
const capabilities = asStringArray(contract.requiredCapabilities, "requiredCapabilities");
|
||||
for (const expected of [
|
||||
"host-codex-process-discovery",
|
||||
"ssh-bridge-contract",
|
||||
"pty-bridge-contract",
|
||||
"stdio-bridge-contract",
|
||||
"prompt-guidance-plan",
|
||||
"trace-summary-plan",
|
||||
"issue-20-board-read-write-entry",
|
||||
"issue-46-brief-read-write-entry",
|
||||
"claudeqq-high-risk-approval-entry",
|
||||
]) {
|
||||
assertCondition(capabilities.includes(expected), `missing required capability ${expected}`, capabilities);
|
||||
}
|
||||
|
||||
const safety = asRecord(contract.safetyBoundary, "safetyBoundary");
|
||||
assertCondition(safety.phaseOneMutationAllowed === false, "phase one must forbid mutation", safety);
|
||||
const forbidden = asStringArray(safety.forbiddenWithoutExplicitUserApproval, "forbiddenWithoutExplicitUserApproval");
|
||||
assertCondition(forbidden.includes("code-queue-backend-restart"), "backend restart must require approval", forbidden);
|
||||
assertCondition(forbidden.includes("code-queue-task-interrupt"), "task interrupt must require approval", forbidden);
|
||||
assertCondition(forbidden.includes("code-queue-task-cancel"), "task cancel must require approval", forbidden);
|
||||
const alwaysForbidden = asStringArray(safety.alwaysForbidden, "alwaysForbidden");
|
||||
assertCondition(alwaysForbidden.includes("print-token-values"), "contract must forbid token output", alwaysForbidden);
|
||||
|
||||
const plan = dataOf(runCli(["commander", "plan", "--dry-run", "--session-id", "primary"], 0));
|
||||
assertCondition(plan.mutation === false, "plan must be non-mutating", plan);
|
||||
assertCondition(asRecord(asRecord(plan.processDiscovery, "processDiscovery").startPlan, "startPlan").enabled === false, "start plan must be disabled", plan);
|
||||
assertCondition(asRecord(plan.bridge, "bridge").mutation === false, "bridge plan must not open bridges", plan);
|
||||
assertCondition(asRecord(plan.traceSummary, "traceSummary").mutation === false, "trace summary plan must be non-mutating", plan);
|
||||
assertCondition(asRecord(plan.issueEntries, "issueEntries").mutation === false, "issue entry plan must be non-mutating", plan);
|
||||
assertCondition(asRecord(plan.claudeqqApproval, "claudeqqApproval").mutation === false, "approval plan must be non-mutating", plan);
|
||||
|
||||
const planWithoutDryRun = dataOf(runCli(["commander", "plan"], 1));
|
||||
assertCondition(planWithoutDryRun.error === "dry-run-required", "plan must require dry-run", planWithoutDryRun);
|
||||
|
||||
const approval = dataOf(runCli([
|
||||
"commander",
|
||||
"approval",
|
||||
"request",
|
||||
"--action",
|
||||
"code-queue-task-interrupt",
|
||||
"--task-id",
|
||||
"task-123",
|
||||
"--reason",
|
||||
"heartbeat expired",
|
||||
"--dry-run",
|
||||
], 0));
|
||||
assertCondition(approval.mutation === false, "approval request must be non-mutating", approval);
|
||||
assertCondition(approval.requiresExplicitUserApproval === true, "approval request must require explicit user approval", approval);
|
||||
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
|
||||
assertCondition(claudeqq.mutation === false, "ClaudeQQ preview must not send", claudeqq);
|
||||
assertCondition(claudeqq.sendImplemented === false, "ClaudeQQ send must not be implemented", claudeqq);
|
||||
|
||||
const invalidApproval = dataOf(runCli(["commander", "approval", "request", "--action", "read-token-file", "--dry-run"], 1));
|
||||
assertCondition(invalidApproval.error === "validation-failed", "unsupported approval action must fail validation", invalidApproval);
|
||||
|
||||
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
||||
for (const snippet of [
|
||||
"不直接重启 Code Queue backend",
|
||||
"不 cancel 或 interrupt 运行中的 Code Queue task",
|
||||
"不读取、打印或持久化 token 明文",
|
||||
"SSH/PTY/stdio",
|
||||
"#20",
|
||||
"#46",
|
||||
"ClaudeQQ",
|
||||
]) {
|
||||
assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"commander contract exposes host Codex service boundary and phase-one no-live-operation flags",
|
||||
"dry-run plan covers process discovery, SSH/PTY/stdio bridge, prompt guidance, trace summary, #20/#46 and ClaudeQQ approval",
|
||||
"non-dry-run plan is rejected",
|
||||
"approval request is dry-run only and rejects unsupported high-risk actions",
|
||||
"reference doc states backend restart, task interrupt/cancel, and token-output prohibitions",
|
||||
],
|
||||
}, null, 2)}\n`);
|
||||
+104
-4
@@ -12,6 +12,7 @@ const defaultTasksLimit = 20;
|
||||
const maxTasksLimit = 100;
|
||||
const steerPromptPreviewChars = 320;
|
||||
const minimaxSubmitModel = "minimax-m2.7";
|
||||
const deepseekSubmitModel = "deepseek-chat";
|
||||
const gptSubmitModel = "gpt-5.5";
|
||||
const submitLockWaitMs = 60_000;
|
||||
const submitLockPollMs = 250;
|
||||
@@ -58,7 +59,7 @@ interface CodexSubmitOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
type SubmitRoute = "minimax-opencode" | "gpt-5.5-codex" | "commander-human-only";
|
||||
type SubmitRoute = "minimax-opencode" | "deepseek-codex" | "gpt-5.5-codex" | "commander-human-only";
|
||||
type SubmitRouteSignalSeverity = "info" | "warning" | "block";
|
||||
|
||||
interface SubmitRouteSignal {
|
||||
@@ -80,7 +81,9 @@ interface SubmitRoutingRecommendation {
|
||||
promptSelfContained: boolean;
|
||||
issueIsNotOnlySource: boolean;
|
||||
noProdRestartSecretOrDbWrite: boolean;
|
||||
noRuntimeCoreOrReleaseWork: boolean;
|
||||
evidenceRequiredByPrompt: boolean;
|
||||
mediumComplexityCandidate: boolean;
|
||||
commanderMustReviewUnread: true;
|
||||
};
|
||||
explicitRequest: {
|
||||
@@ -92,6 +95,27 @@ interface SubmitRoutingRecommendation {
|
||||
dryRunOnly: true;
|
||||
doesNotChangeSubmittedPayload: true;
|
||||
prodMiniMaxAssumedAvailable: false;
|
||||
prodDeepSeekAssumedAvailable: false;
|
||||
runtimeAdmissionUnchanged: true;
|
||||
};
|
||||
policyContract: {
|
||||
selectionPrinciples: string[];
|
||||
concurrency: {
|
||||
gpt55Routine: number;
|
||||
gpt55BurstMax: number;
|
||||
minimaxSimpleMax: number;
|
||||
deepseekMediumDefault: number;
|
||||
};
|
||||
modelTiers: Array<{
|
||||
model: string;
|
||||
runner: "opencode" | "codex";
|
||||
taskRisk: string;
|
||||
requiredGuards: string[];
|
||||
}>;
|
||||
externalProvider429: {
|
||||
commanderAction: string;
|
||||
interveneWhen: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -579,6 +603,7 @@ function normalizeSubmitModel(value: string | null | undefined): string {
|
||||
const lower = raw.toLowerCase();
|
||||
const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower;
|
||||
if (leaf === minimaxSubmitModel || leaf === "m2.7") return minimaxSubmitModel;
|
||||
if (leaf === deepseekSubmitModel || leaf === "deepseek") return deepseekSubmitModel;
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -606,7 +631,7 @@ function regexEvidenceWithoutNegatedContext(text: string, patterns: RegExp[], li
|
||||
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;
|
||||
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;
|
||||
@@ -619,6 +644,47 @@ function routeSignal(id: string, severity: SubmitRouteSignalSeverity, evidence:
|
||||
return { id, severity, matched: evidence.length > 0, evidence, message };
|
||||
}
|
||||
|
||||
function submitPolicyContract(): SubmitRoutingRecommendation["policyContract"] {
|
||||
return {
|
||||
selectionPrinciples: [
|
||||
"Use GPT-5.5 for high-risk, runtime/core, security, CI/CD, deploy, release, and final quality calls.",
|
||||
"Use DeepSeek for self-contained medium-complexity work with limited write scope and verifiable tests.",
|
||||
"Use MiniMax only for simple, low-risk, self-contained work with external evidence and commander review.",
|
||||
"Keep prod restart, secret access, DB writes, destructive Git, and running-task control with the commander or human.",
|
||||
],
|
||||
concurrency: {
|
||||
gpt55Routine: 5,
|
||||
gpt55BurstMax: 10,
|
||||
minimaxSimpleMax: 10,
|
||||
deepseekMediumDefault: 5,
|
||||
},
|
||||
modelTiers: [
|
||||
{
|
||||
model: gptSubmitModel,
|
||||
runner: "codex",
|
||||
taskRisk: "high-risk-or-complex",
|
||||
requiredGuards: ["bounded ownership", "multi-signal verification", "no implicit prod rollout"],
|
||||
},
|
||||
{
|
||||
model: deepseekSubmitModel,
|
||||
runner: "codex",
|
||||
taskRisk: "medium-complexity",
|
||||
requiredGuards: ["self-contained prompt", "limited write scope", "contract/unit verification", "commander review"],
|
||||
},
|
||||
{
|
||||
model: minimaxSubmitModel,
|
||||
runner: "opencode",
|
||||
taskRisk: "simple-low-risk",
|
||||
requiredGuards: ["issue is auxiliary only", "evidence required", "no prod/secrets/DB writes", "diff and test review"],
|
||||
},
|
||||
],
|
||||
externalProvider429: {
|
||||
commanderAction: "wait-while-exponential-backoff-is-healthy",
|
||||
interveneWhen: ["heartbeat expired", "retry state machine stuck", "task lost", "retry attempts exhausted"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRoutingRecommendation {
|
||||
const prompt = options.prompt;
|
||||
const lower = prompt.toLowerCase();
|
||||
@@ -633,7 +699,7 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
/\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, [
|
||||
const runtimeCore = regexEvidenceWithoutNegatedContext(lower, [
|
||||
/\bcode[- ]queue\s+(?:runtime|scheduler|backend|execution|runner)\b/gu,
|
||||
/\bbackend-core\b/gu,
|
||||
/\bprovider-gateway\b/gu,
|
||||
@@ -672,6 +738,14 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
/文档/gu,
|
||||
/轻量/gu,
|
||||
]);
|
||||
const mediumComplexityEvidence = regexEvidence(lower, [
|
||||
/\bfront[- ]?end\b|\bfrontend\b|\breact\b|\btsx\b|\bcss\b|\bui\b|\bcomponent\b/gu,
|
||||
/\buser[- ]service\b|\bservice\s+module\b/gu,
|
||||
/\blocal\s+(?:module|helper|cli|tool)\b/gu,
|
||||
/\bbounded\s+(?:bug\s*)?fix\b|\bsmall\s+(?:bug\s*)?fix\b/gu,
|
||||
/\bunit\s+test\b|\bcontract\s+guard\b/gu,
|
||||
/中等复杂|中等风险|前端|组件|样式|局部(?:模块|修复)|用户服务|契约守卫/gu,
|
||||
]);
|
||||
const evidenceRequest = regexEvidence(lower, [
|
||||
/\bevidence\b/gu,
|
||||
/\bverification\b|\bverified\b|\bvalidate\b|\bvalidation\b/gu,
|
||||
@@ -699,7 +773,7 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
/\btruncate\s+table\b/gu,
|
||||
/\bdelete\s+from\b/gu,
|
||||
]);
|
||||
const crossModule = regexEvidence(lower, [
|
||||
const crossModule = regexEvidenceWithoutNegatedContext(lower, [
|
||||
/\bcross[- ]module\b/gu,
|
||||
/\barchitecture\b|\barchitectural\b/gu,
|
||||
/\brelease\/v1\b/gu,
|
||||
@@ -714,13 +788,21 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
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 noRuntimeCoreOrReleaseWork = runtimeCore.length === 0 && crossModule.length === 0;
|
||||
const evidenceRequiredByPrompt = evidenceRequest.length > 0;
|
||||
const mediumComplexityCandidate = promptSelfContained
|
||||
&& issueIsNotOnlySource
|
||||
&& noProdRestartSecretOrDbWrite
|
||||
&& noRuntimeCoreOrReleaseWork
|
||||
&& evidenceRequiredByPrompt
|
||||
&& mediumComplexityEvidence.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("medium-complexity-verifiable", "info", mediumComplexityEvidence, "Mentions bounded medium-complexity work such as frontend, local CLI/helper, user-service module, or contract guard changes."),
|
||||
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."),
|
||||
@@ -742,6 +824,18 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
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 (mediumComplexityCandidate) {
|
||||
route = "deepseek-codex";
|
||||
recommendedRunner = "codex";
|
||||
recommendedModel = deepseekSubmitModel;
|
||||
confidence = "high";
|
||||
reason = "The prompt looks self-contained, medium-complexity, and verifiable without production/state privileges; it is a DeepSeek/Codex candidate after commander review.";
|
||||
} else if (mediumComplexityEvidence.length > 0 && issueIsNotOnlySource && noProdRestartSecretOrDbWrite && noRuntimeCoreOrReleaseWork) {
|
||||
route = "deepseek-codex";
|
||||
recommendedRunner = "codex";
|
||||
recommendedModel = deepseekSubmitModel;
|
||||
confidence = "medium";
|
||||
reason = "The prompt has medium-complexity signals, but the commander should tighten self-contained context, write scope, and verification requirements before relying on DeepSeek.";
|
||||
} else if (promptSelfContained && issueIsNotOnlySource && evidenceRequiredByPrompt && lowRiskEvidence.length > 0) {
|
||||
route = "minimax-opencode";
|
||||
recommendedRunner = "opencode";
|
||||
@@ -773,7 +867,9 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
promptSelfContained,
|
||||
issueIsNotOnlySource,
|
||||
noProdRestartSecretOrDbWrite,
|
||||
noRuntimeCoreOrReleaseWork,
|
||||
evidenceRequiredByPrompt,
|
||||
mediumComplexityCandidate,
|
||||
commanderMustReviewUnread: true,
|
||||
},
|
||||
explicitRequest: {
|
||||
@@ -785,7 +881,10 @@ function submitRoutingRecommendation(options: CodexSubmitOptions): SubmitRouting
|
||||
dryRunOnly: true,
|
||||
doesNotChangeSubmittedPayload: true,
|
||||
prodMiniMaxAssumedAvailable: false,
|
||||
prodDeepSeekAssumedAvailable: false,
|
||||
runtimeAdmissionUnchanged: true,
|
||||
},
|
||||
policyContract: submitPolicyContract(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2495,6 +2594,7 @@ function codexSubmitTask(args: string[]): unknown {
|
||||
commands: {
|
||||
submitAsRequested: "remove --dry-run to submit exactly this payload",
|
||||
minimaxCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${minimaxSubmitModel} --dry-run`,
|
||||
deepseekCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${deepseekSubmitModel} --dry-run`,
|
||||
gptCandidate: `bun scripts/cli.ts codex submit --prompt-file <path> --model ${gptSubmitModel} --dry-run`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
const requiredDryRunMessage = "This first-phase commander contract only supports dry-run planning; live daemon/control operations are not implemented.";
|
||||
|
||||
const highRiskActions = [
|
||||
"code-queue-backend-restart",
|
||||
"code-queue-backend-rebuild",
|
||||
"code-queue-execution-plane-restart",
|
||||
"code-queue-task-interrupt",
|
||||
"code-queue-task-cancel",
|
||||
"prod-runtime-mutation",
|
||||
] as const;
|
||||
|
||||
type HighRiskAction = typeof highRiskActions[number];
|
||||
|
||||
function hasFlag(args: string[], flag: string): boolean {
|
||||
return args.includes(flag);
|
||||
}
|
||||
|
||||
function optionValue(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return undefined;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function isHelpToken(value: string | undefined): boolean {
|
||||
return value === "help" || value === "--help" || value === "-h";
|
||||
}
|
||||
|
||||
function isHighRiskAction(value: string): value is HighRiskAction {
|
||||
return highRiskActions.some((action) => action === value);
|
||||
}
|
||||
|
||||
function commanderHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "commander",
|
||||
output: "json",
|
||||
description: "First-phase source/contract stub for the host Codex commander control microservice; no daemon or live control action is implemented.",
|
||||
usage: [
|
||||
"bun scripts/cli.ts commander contract",
|
||||
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
|
||||
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
|
||||
],
|
||||
highRiskActions,
|
||||
reference: "docs/reference/host-codex-commander.md",
|
||||
};
|
||||
}
|
||||
|
||||
export function commanderContract(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
phase: "source-contract",
|
||||
serviceId: "host-codex-commander",
|
||||
currentImplementation: "cli-contract-stub-only",
|
||||
daemonImplemented: false,
|
||||
liveOperationsImplemented: false,
|
||||
purpose: "Keep a host Codex commander session observable and controllable through a future direct-managed microservice without replacing Code Queue runners.",
|
||||
ownershipBoundary: {
|
||||
hostCodexProcess: "Long-lived Codex process on the master server host.",
|
||||
controlMicroservice: "Future direct-managed bridge that records state, mediates PTY/stdio/SSH streams, injects prompts, and summarizes traces.",
|
||||
codeQueue: "Remains the task execution plane; the commander only supervises through existing safe CLI/API contracts.",
|
||||
claudeqq: "Approval and user-notification path for high-risk actions.",
|
||||
},
|
||||
requiredCapabilities: [
|
||||
"host-codex-process-discovery",
|
||||
"host-codex-start-plan",
|
||||
"ssh-bridge-contract",
|
||||
"pty-bridge-contract",
|
||||
"stdio-bridge-contract",
|
||||
"prompt-guidance-plan",
|
||||
"trace-summary-plan",
|
||||
"issue-20-board-read-write-entry",
|
||||
"issue-46-brief-read-write-entry",
|
||||
"claudeqq-high-risk-approval-entry",
|
||||
],
|
||||
apiContract: {
|
||||
health: "GET /health",
|
||||
contract: "GET /api/commander/contract",
|
||||
sessions: "GET /api/commander/sessions",
|
||||
sessionPlan: "POST /api/commander/sessions/:sessionId/plan-start",
|
||||
promptPlan: "POST /api/commander/sessions/:sessionId/prompt-plan",
|
||||
traceSummary: "GET /api/commander/trace-summary?taskId=<taskId>",
|
||||
issueWritePlan: "POST /api/commander/issues/:issueNumber/write-plan",
|
||||
approvalRequest: "POST /api/commander/approvals",
|
||||
},
|
||||
stateModel: {
|
||||
sessionStates: ["unknown", "discovered", "planned", "starting", "running", "attention_required", "stopping", "stopped", "degraded"],
|
||||
promptStates: ["draft", "planned", "queued_for_injection", "injected", "rejected", "failed"],
|
||||
approvalStates: ["draft", "requested", "approved", "rejected", "expired", "consumed"],
|
||||
storageRoot: ".state/commander/",
|
||||
redactionPolicy: "Never persist or print token, secret, password, key, cookie, or authorization values in cleartext.",
|
||||
},
|
||||
safetyBoundary: safetyBoundary(),
|
||||
};
|
||||
}
|
||||
|
||||
function safetyBoundary(): Record<string, unknown> {
|
||||
return {
|
||||
phaseOneMutationAllowed: false,
|
||||
forbiddenWithoutExplicitUserApproval: highRiskActions,
|
||||
alwaysForbidden: [
|
||||
"print-token-values",
|
||||
"read-token-files-for-display",
|
||||
"direct-database-state-patch",
|
||||
"bypass-code-queue-backend-confirmation-policy",
|
||||
"replace-code-queue-runner",
|
||||
"deploy-or-restart-production-runtime-from-this-contract-stub",
|
||||
],
|
||||
confirmationPolicy: "High-risk actions must draft a ClaudeQQ request, wait for explicit user approval, bind approval to one exact action, and record the decision before any future live executor may proceed.",
|
||||
};
|
||||
}
|
||||
|
||||
function processDiscoveryPlan(sessionId: string): Record<string, unknown> {
|
||||
return {
|
||||
sessionId,
|
||||
mutation: false,
|
||||
signals: [
|
||||
".state/commander/sessions/<sessionId>.json",
|
||||
"host process table filtered by executable and cwd markers",
|
||||
"PTY/stdio bridge heartbeat file",
|
||||
"last prompt/trace event sequence",
|
||||
],
|
||||
startPlan: {
|
||||
enabled: false,
|
||||
commandShape: "codex <reviewed host profile> --cwd /workspace/unidesk",
|
||||
supervisor: "future direct-managed microservice on the master server host",
|
||||
reason: requiredDryRunMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function bridgePlan(): Record<string, unknown> {
|
||||
return {
|
||||
mutation: false,
|
||||
adapters: {
|
||||
ssh: {
|
||||
purpose: "Reach provider hosts through existing UniDesk Host SSH / WSL SSH maintenance bridge.",
|
||||
allowedUse: ["readonly diagnostics", "bounded reviewed maintenance commands", "future approved recovery commands"],
|
||||
},
|
||||
pty: {
|
||||
purpose: "Keep the host Codex interactive session alive and observable.",
|
||||
allowedUse: ["windowed stdout/stderr capture", "prompt injection after policy checks", "heartbeat"],
|
||||
},
|
||||
stdio: {
|
||||
purpose: "Bridge non-interactive Codex or helper subprocesses with bounded transcript capture.",
|
||||
allowedUse: ["contracted command execution", "trace collection", "structured result capture"],
|
||||
},
|
||||
},
|
||||
guardrails: [
|
||||
"bounded output by default",
|
||||
"redact secret-like values before state persistence",
|
||||
"persist event sequence before acknowledging prompt injection",
|
||||
"never open an interactive shell from dry-run commands",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function issueEntryPlan(): Record<string, unknown> {
|
||||
return {
|
||||
mutation: false,
|
||||
issues: [
|
||||
{
|
||||
number: 20,
|
||||
role: "Code Queue total board",
|
||||
readEntrypoints: ["bun scripts/cli.ts gh issue board-audit --board-issue 20 --dry-run", "bun scripts/cli.ts gh issue board-row list --board-issue 20"],
|
||||
writeEntrypoints: ["bun scripts/cli.ts gh issue board-row update <issueNumber> --board-issue 20 --field <field> --value <text> --expect-body-sha <sha>"],
|
||||
},
|
||||
{
|
||||
number: 46,
|
||||
role: "daily commander brief",
|
||||
readEntrypoints: ["bun scripts/cli.ts gh issue read 46 --json body,title,state,updatedAt"],
|
||||
writeEntrypoints: ["bun scripts/cli.ts gh issue update 46 --body-profile commander-brief --body-file <file> --expect-updated-at <ts>"],
|
||||
},
|
||||
],
|
||||
writeGuards: ["body-file-only", "dry-run-first", "body-sha-or-updated-at-required", "no-token-output"],
|
||||
};
|
||||
}
|
||||
|
||||
function traceSummaryPlan(): Record<string, unknown> {
|
||||
return {
|
||||
mutation: false,
|
||||
sources: [
|
||||
"bounded host Codex PTY/stdio event window",
|
||||
"Code Queue task trace via codex task --trace",
|
||||
"Code Queue output pages via codex output",
|
||||
"commander approval and prompt event JSONL",
|
||||
],
|
||||
summaryShape: {
|
||||
taskId: "string|null",
|
||||
sessionId: "string",
|
||||
lastSeq: "number",
|
||||
status: "running|attention_required|blocked|terminal|unknown",
|
||||
keyEvents: "bounded array",
|
||||
openQuestions: "bounded array",
|
||||
recommendedNextActions: "bounded array",
|
||||
redactionsApplied: "number",
|
||||
},
|
||||
outputPolicy: "Default summaries must omit raw transcript text unless explicitly requested by a future reviewed endpoint.",
|
||||
};
|
||||
}
|
||||
|
||||
function promptGuidancePlan(): Record<string, unknown> {
|
||||
return {
|
||||
mutation: false,
|
||||
stages: [
|
||||
"classify intent and risk",
|
||||
"attach current #20/#46/task context summary",
|
||||
"apply forbidden-action guard",
|
||||
"redact secret-like strings",
|
||||
"persist prompt plan",
|
||||
"inject only through future live executor after policy checks",
|
||||
],
|
||||
highRiskEscalation: "code-queue restart/rebuild, task interrupt/cancel, production mutation, token access, and destructive Git operations require ClaudeQQ approval first.",
|
||||
};
|
||||
}
|
||||
|
||||
function commanderPlan(args: string[]): Record<string, unknown> {
|
||||
if (!hasFlag(args, "--dry-run")) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "dry-run-required",
|
||||
message: requiredDryRunMessage,
|
||||
command: "bun scripts/cli.ts commander plan --dry-run",
|
||||
};
|
||||
}
|
||||
const sessionId = optionValue(args, "--session-id") ?? "primary";
|
||||
return {
|
||||
ok: true,
|
||||
phase: "source-contract",
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
serviceId: "host-codex-commander",
|
||||
processDiscovery: processDiscoveryPlan(sessionId),
|
||||
bridge: bridgePlan(),
|
||||
promptGuidance: promptGuidancePlan(),
|
||||
traceSummary: traceSummaryPlan(),
|
||||
issueEntries: issueEntryPlan(),
|
||||
claudeqqApproval: {
|
||||
mutation: false,
|
||||
commandShape: "bun scripts/cli.ts commander approval request --action <action> --dry-run",
|
||||
highRiskActions,
|
||||
},
|
||||
safetyBoundary: safetyBoundary(),
|
||||
};
|
||||
}
|
||||
|
||||
function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
||||
if (!hasFlag(args, "--dry-run")) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "dry-run-required",
|
||||
message: requiredDryRunMessage,
|
||||
command: "bun scripts/cli.ts commander approval request --action <action> --dry-run",
|
||||
};
|
||||
}
|
||||
const action = optionValue(args, "--action");
|
||||
if (action === undefined) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "validation-failed",
|
||||
message: "--action is required",
|
||||
highRiskActions,
|
||||
};
|
||||
}
|
||||
if (!isHighRiskAction(action)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "validation-failed",
|
||||
message: `unsupported high-risk action: ${action}`,
|
||||
highRiskActions,
|
||||
};
|
||||
}
|
||||
const reason = optionValue(args, "--reason") ?? "operator-supplied reason required before live execution";
|
||||
const taskId = optionValue(args, "--task-id") ?? null;
|
||||
return {
|
||||
ok: true,
|
||||
phase: "source-contract",
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
action,
|
||||
taskId,
|
||||
reason,
|
||||
requiresExplicitUserApproval: true,
|
||||
claudeqq: {
|
||||
mutation: false,
|
||||
endpointShape: "POST /api/microservices/claudeqq/proxy/api/push/text",
|
||||
target: "configured primary user private chat",
|
||||
messageTemplate: `Approval required for ${action}. Reason: ${reason}. Reply with explicit approval id before execution.`,
|
||||
sendImplemented: false,
|
||||
},
|
||||
approvalRecordShape: {
|
||||
id: "commander-approval-<stable-id>",
|
||||
action,
|
||||
taskId,
|
||||
reason,
|
||||
status: "draft",
|
||||
approvedBy: null,
|
||||
approvedAt: null,
|
||||
expiresAt: "future reviewed timeout",
|
||||
},
|
||||
blockedUntilApproved: [action],
|
||||
};
|
||||
}
|
||||
|
||||
export function runCommanderCommand(args: string[]): Record<string, unknown> {
|
||||
const [sub, second] = args;
|
||||
if (sub === undefined || isHelpToken(sub)) return commanderHelp();
|
||||
if (sub === "contract") return commanderContract();
|
||||
if (sub === "plan") return commanderPlan(args.slice(1));
|
||||
if (sub === "approval" && second === "request") return commanderApprovalRequest(args.slice(2));
|
||||
return {
|
||||
ok: false,
|
||||
error: "unsupported-command",
|
||||
message: `Unsupported commander command: ${args.join(" ")}`,
|
||||
help: commanderHelp(),
|
||||
};
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
|
||||
{ command: "commander contract|plan --dry-run|approval request --dry-run", description: "First-phase host Codex commander source/contract design stub; returns boundaries and approval plans without starting daemons or executing live control actions." },
|
||||
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
|
||||
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
|
||||
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
|
||||
@@ -185,6 +186,26 @@ function providerHelp(): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
function commanderHelp(): unknown {
|
||||
return {
|
||||
command: "commander contract|plan|approval",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts commander contract",
|
||||
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
|
||||
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
|
||||
],
|
||||
description: "Inspect the first-phase source/contract design for the future host Codex commander microservice.",
|
||||
boundary: [
|
||||
"phase one is contract-only and never starts a daemon",
|
||||
"dry-run commands never open SSH, PTY, or stdio bridges",
|
||||
"high-risk actions only produce a ClaudeQQ approval draft",
|
||||
"token and secret values must never be printed",
|
||||
],
|
||||
reference: "docs/reference/host-codex-commander.md",
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleHelp(): unknown {
|
||||
return {
|
||||
command: "schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup",
|
||||
@@ -340,6 +361,7 @@ export function staticNamespaceHelp(args: string[]): unknown | null {
|
||||
if (top === "microservice") return microserviceHelp();
|
||||
if (top === "decision" || top === "decision-center") return decisionHelp();
|
||||
if (top === "provider") return providerHelp();
|
||||
if (top === "commander") return commanderHelp();
|
||||
if (top === "schedule") return scheduleHelp();
|
||||
if (top === "codex") return codexHelp();
|
||||
if (top === "job") return jobHelp();
|
||||
|
||||
Reference in New Issue
Block a user