fix(code-queue): add runner skills sync contract

This commit is contained in:
Codex
2026-05-23 04:39:44 +00:00
parent 9418cc4f12
commit 1358803c9a
8 changed files with 709 additions and 29 deletions
+180 -12
View File
@@ -305,7 +305,12 @@ interface CodexPrPreflightOptions {
full: boolean;
}
type CodeQueuePrPreflightFailureKind = "auth-missing" | "proxy-gap" | "git-remote-gap" | "control-plane-missing" | "target-stack-not-running";
interface CodexSkillsSyncOptions {
dryRun: boolean;
full: boolean;
}
type CodeQueuePrPreflightFailureKind = "auth-missing" | "runner-skills-blocker" | "proxy-gap" | "git-remote-gap" | "control-plane-missing" | "target-stack-not-running";
type CodeQueueObservationGapKind = "runner-local-observation-gap" | "control-plane-observation-gap" | null;
interface CodeQueuePrPreflightTransport {
@@ -1742,6 +1747,17 @@ function parsePrPreflightOptions(args: string[]): CodexPrPreflightOptions {
};
}
function parseSkillsSyncOptions(args: string[]): CodexSkillsSyncOptions {
assertKnownOptions(args, {
flags: ["--dry-run", "--dryRun", "--full", "--raw"],
}, "codex skills-sync");
const dryRun = hasFlag(args, "--dry-run") || hasFlag(args, "--dryRun");
return {
dryRun,
full: hasFlag(args, "--full") || hasFlag(args, "--raw"),
};
}
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
assertKnownOptions(args, {
flags: ["--dry-run", "--no-call", "--include-prompt"],
@@ -3072,6 +3088,56 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
};
}
function compactSkillPathReport(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
path: record.path ?? null,
approved: record.approved ?? false,
exists: record.exists ?? false,
directory: record.directory ?? false,
readable: record.readable ?? false,
writable: record.writable ?? false,
readonly: record.readonly ?? false,
mountPoint: record.mountPoint ?? null,
skillCount: record.skillCount ?? 0,
requiredSkills: Array.isArray(record.requiredSkills) ? record.requiredSkills.map(String) : [],
missingSkills: Array.isArray(record.missingSkills) ? record.missingSkills.map(String) : [],
error: record.error ?? null,
};
}
function compactSkillsSyncStatus(value: unknown, full = false): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
const source = compactSkillPathReport(record.source);
const target = compactSkillPathReport(record.target);
const compact: Record<string, unknown> = {
ok: record.ok ?? false,
degraded: record.degraded ?? true,
blocker: record.blocker ?? null,
checkedAt: record.checkedAt ?? null,
mode: record.mode ?? "dry-run",
dryRun: record.dryRun ?? true,
mutation: record.mutation ?? false,
syncMode: record.syncMode ?? null,
source,
target,
expected: record.expected ?? null,
counts: record.counts ?? null,
missing: record.missing ?? null,
permissionFailures: Array.isArray(record.permissionFailures) ? record.permissionFailures.slice(0, full ? undefined : 4) : [],
permissionFailureCount: Array.isArray(record.permissionFailures) ? record.permissionFailures.length : 0,
pathSpelling: record.pathSpelling ?? null,
plannedActions: record.plannedActions ?? null,
instructions: Array.isArray(record.instructions) ? record.instructions.map(String).slice(0, full ? undefined : 4) : [],
commands: record.commands ?? null,
valuesPrinted: record.valuesPrinted ?? false,
};
if (full) compact.rawSkillsSync = record;
return compact;
}
function codeQueueDevReady(): unknown {
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/dev-ready")));
const devReady = asRecord(response.body.devReady) ?? {};
@@ -3085,10 +3151,101 @@ function codeQueueDevReady(): unknown {
codexConfig: devReady.codexConfig ?? null,
ssh: devReady.ssh ?? null,
skills: compactSkillsStatus(devReady.skills),
skillsSync: compactSkillsSyncStatus(devReady.skillsSync),
},
commands: {
health: "bun scripts/cli.ts codex dev-ready",
raw: "bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw",
skillsSync: "bun scripts/cli.ts codex skills-sync --dry-run",
},
};
}
function codeQueueSkillsSync(args: string[]): unknown {
const options = parseSkillsSyncOptions(args);
if (!options.dryRun) {
return {
ok: false,
failureKind: "dry-run-required",
runnerDisposition: "business-failed",
message: "codex skills-sync is dry-run only; pass --dry-run",
mutation: false,
commands: {
dryRun: "bun scripts/cli.ts codex skills-sync --dry-run",
full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
},
};
}
const rawResponse = coreInternalFetch(codeQueueProxyPath("/api/skills-sync?dryRun=1"));
const response = asRecord(rawResponse);
if (response?.ok !== true) {
const targetStack = asRecord(response?.targetStack);
const observed = asRecord(response?.observed);
const missingContainers = Array.isArray(targetStack?.missingContainers) ? targetStack.missingContainers.map(String) : [];
const relatedContainers = Array.isArray(targetStack?.relatedContainers) ? targetStack.relatedContainers : [];
return {
ok: false,
dryRun: true,
mutation: false,
runnerDisposition: response?.runnerDisposition ?? "infra-blocked",
failureKind: response?.failureKind ?? "control-plane-missing",
degradedReason: response?.degradedReason ?? "backend-core-proxy-unavailable",
message: response?.message ?? response?.stderrTail ?? response?.stdoutTail ?? "Code Queue skills sync dry-run could not reach the control plane",
controlPlane: {
mode: "local-backend-core",
localBackendCoreMissing: response?.failureKind === "target-stack-not-running",
schedulerStateChanged: false,
liveRunnerHostPathMutated: false,
},
targetStackSummary: targetStack === null ? null : {
missingContainers,
missingContainerCount: missingContainers.length,
relatedContainerCount: relatedContainers.length,
verifyOnlyObserved: targetStack.verifyOnlyObserved ?? false,
},
observed: observed === null ? null : {
commandExitCode: observed.commandExitCode ?? null,
stderrTail: typeof observed.stderrTail === "string" ? textView(observed.stderrTail, false, 600) : null,
},
outputPolicy: {
default: "compact-skills-sync-control-plane-failure",
unrelatedDiagnosticsOmitted: true,
full: "use microservice health/proxy commands only when control-plane diagnostics are needed",
},
commands: {
retry: "bun scripts/cli.ts codex skills-sync --dry-run",
full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
health: "bun scripts/cli.ts microservice health code-queue",
rawProxy: "bun scripts/cli.ts microservice proxy code-queue /api/skills-sync?dryRun=1 --raw --full",
},
};
}
const body = asRecord(response.body);
if (body?.ok !== true) {
return {
ok: false,
dryRun: true,
mutation: false,
runnerDisposition: "infra-blocked",
failureKind: "runtime-skills-sync-missing",
degradedReason: "runtime-skills-sync-response-invalid",
message: "Code Queue skills sync dry-run response did not include a valid body",
upstream: { ok: response.ok, status: response.status ?? null },
bodyPreview: body,
commands: {
retry: "bun scripts/cli.ts codex skills-sync --dry-run",
rawProxy: "bun scripts/cli.ts microservice proxy code-queue /api/skills-sync?dryRun=1 --raw --full",
},
};
}
const skillsSync = asRecord(body.skillsSync);
return {
upstream: { ok: response.ok, status: response.status ?? null },
skillsSync: compactSkillsSyncStatus(skillsSync, options.full),
outputPolicy: {
default: "compact-skills-sync-dry-run",
full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
mutation: false,
},
};
}
@@ -3457,6 +3614,8 @@ function maybeFullPrPreflightObservations(options: CodexPrPreflightOptions, loca
function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> {
const pull = asRecord(preflight.pullRequestDelivery) ?? {};
const skills = compactSkillsStatus(preflight.skills);
const skillsSync = compactSkillsSyncStatus(preflight.skillsSync, options.full);
const tools = asRecord(pull.tools) ?? {};
const unideskGhCli = compactUniDeskGhCliStatus(pull.unideskGhCli);
const authBrokerRuntime = compactAuthBrokerRuntimeStatus(pull.authBroker);
@@ -3472,20 +3631,26 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
const limitations = Array.isArray(pull.limitations) ? pull.limitations.map(String) : [];
const risks = Array.isArray(pull.risks) ? pull.risks.map(String) : [];
const ok = preflight.ok === true && tokenCoverage.ok === true;
const skillsBlocked = skills !== null && skills.ok !== true;
const skillsSyncBlocked = skillsSync !== null && skillsSync.ok !== true;
const failureKind = !tokenCoverage.ok
? "auth-missing"
: limitations.some((item) => item.includes("git ls-remote") || item.includes("git push --dry-run failed"))
? "git-remote-gap"
: !preflight.ok
? "proxy-gap"
: null;
: skillsBlocked || skillsSyncBlocked
? "runner-skills-blocker"
: limitations.some((item) => item.includes("git ls-remote") || item.includes("git push --dry-run failed"))
? "git-remote-gap"
: !preflight.ok
? "proxy-gap"
: null;
const degradedReason = failureKind === "auth-missing"
? "auth-broker-needed"
: failureKind === "git-remote-gap"
? "git remote probe failed"
: failureKind === "proxy-gap"
? limitations.find((item) => item.includes("proxy") || item.includes("auth") || item.includes("egress") || item.includes("reachable")) ?? null
: null;
: failureKind === "runner-skills-blocker"
? typeof skillsSync?.blocker === "string" ? skillsSync.blocker : typeof skills?.blocker === "string" ? skills.blocker : "runner-skills-degraded"
: failureKind === "git-remote-gap"
? "git remote probe failed"
: failureKind === "proxy-gap"
? limitations.find((item) => item.includes("proxy") || item.includes("auth") || item.includes("egress") || item.includes("reachable")) ?? null
: null;
const defaultPushDryRunRef = "refs/heads/probe/code-queue-pr-capability-dryrun";
const pushDryRunRef = options.pushDryRunRef ?? defaultPushDryRunRef;
const targetBranch = "master";
@@ -3506,6 +3671,8 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
cwd: preflight.cwd ?? null,
pid: preflight.pid ?? null,
},
skills,
skillsSync,
tokenCoverage,
authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli),
authScopeSummary,
@@ -4138,6 +4305,7 @@ export async function runCodeQueueCommand(config: UniDeskConfig, args: string[])
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
return codeQueueDevReady();
}
if (action === "skills-sync") return codeQueueSkillsSync(args.slice(1));
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1), { config });
if (action === "output") {
const taskId = requireTaskId(taskIdArg, "codex output");
@@ -4173,5 +4341,5 @@ export async function runCodeQueueCommand(config: UniDeskConfig, args: string[])
const taskId = requireTaskId(taskIdArg, "codex steer");
return codexSteerTask(taskId, args.slice(2));
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, pr-preflight, runtime-preflight, queues, queue list, queue create, queue merge, move, steer, interrupt, cancel");
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, skills-sync, pr-preflight, runtime-preflight, queues, queue list, queue create, queue merge, move, steer, interrupt, cancel");
}
+5 -3
View File
@@ -53,7 +53,8 @@ export function rootHelp(): unknown {
{ 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." },
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Disabled legacy Code Queue deploy path; use the dev-only artifact consumer instead." },
{ 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, while real success only confirms the write and task id." },
{ 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 skills-sync --dry-run [--full]", description: "Inspect the controlled runner skills hostPath lifecycle contract without copying files, restarting services, reading secrets, or mutating live runner paths." },
{ 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, skills lifecycle health, 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; --detail is still capped, while --full/trace/output explicitly expand evidence." },
{ 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, tiny local sections, 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; default caps large limits/text previews, --full-text explicitly expands one seq window." },
@@ -244,7 +245,7 @@ function scheduleHelp(): unknown {
function codexHelp(): unknown {
return {
command: "codex deploy|submit|task|tasks|output|read|dev-ready|pr-preflight|judge|steer|interrupt|cancel|queues|queue|move",
command: "codex deploy|submit|task|tasks|output|read|dev-ready|skills-sync|pr-preflight|judge|steer|interrupt|cancel|queues|queue|move",
output: "json",
usage: [
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
@@ -256,6 +257,7 @@ function codexHelp(): unknown {
"bun scripts/cli.ts codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]",
"bun scripts/cli.ts codex read <taskId>",
"bun scripts/cli.ts codex dev-ready",
"bun scripts/cli.ts codex skills-sync --dry-run [--full]",
"bun scripts/cli.ts 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]",
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run] [--no-retry|--retry-attempts N]",
@@ -280,7 +282,7 @@ function codexHelp(): unknown {
},
disclosure: {
defaultPolicy: "low-noise JSON by default; write commands confirm persistence, list/detail/output commands return bounded summaries with drill-down commands",
expand: ["codex task <taskId> --full", "codex task <taskId> --trace --limit N", "codex output <taskId> --after-seq N --limit N --full-text", "codex tasks --view full --limit N"],
expand: ["codex task <taskId> --full", "codex task <taskId> --trace --limit N", "codex output <taskId> --after-seq N --limit N --full-text", "codex tasks --view full --limit N", "codex skills-sync --dry-run --full"],
},
description: "Operate Code Queue through the stable backend-core private proxy path. Real submit/steer success is a low-noise write confirmation and does not echo prompt text.",
};
+85 -2
View File
@@ -367,6 +367,86 @@ function compactQueueHealth(value: unknown): Record<string, unknown> | null {
};
}
function compactSkillAvailability(value: unknown): Record<string, unknown> | null {
const skills = asRecord(value);
if (skills === null) return null;
return {
ok: skills.ok ?? false,
path: skills.path ?? null,
source: skills.source ?? null,
target: skills.target ?? null,
mountPoint: skills.mountPoint ?? null,
exists: skills.exists ?? false,
available: skills.available ?? false,
degraded: skills.degraded ?? true,
blocker: skills.blocker ?? null,
readonly: skills.readonly ?? false,
skillCount: skills.skillCount ?? 0,
requiredSkills: Array.isArray(skills.requiredSkills) ? skills.requiredSkills.map(String) : [],
missingSkills: Array.isArray(skills.missingSkills) ? skills.missingSkills.map(String) : [],
valuesPrinted: skills.valuesPrinted ?? false,
pathSpelling: compactRecordFields(asRecord(skills.pathSpelling), [
"expectedTarget",
"forbiddenPathChecked",
"forbiddenPathExists",
"forbiddenPathMustNotBeUsed",
]),
repairHint: skills.repairHint ?? null,
};
}
function compactSkillSync(value: unknown): Record<string, unknown> | null {
const sync = asRecord(value);
if (sync === null) return null;
const source = compactRecordFields(asRecord(sync.source), [
"path",
"approved",
"exists",
"directory",
"readable",
"writable",
"readonly",
"mountPoint",
"skillCount",
"requiredSkills",
"missingSkills",
"error",
]);
const target = compactRecordFields(asRecord(sync.target), [
"path",
"approved",
"exists",
"directory",
"readable",
"writable",
"readonly",
"mountPoint",
"skillCount",
"requiredSkills",
"missingSkills",
"error",
]);
const permissionFailures = Array.isArray(sync.permissionFailures) ? sync.permissionFailures : [];
return {
ok: sync.ok ?? false,
degraded: sync.degraded ?? true,
blocker: sync.blocker ?? null,
mode: sync.mode ?? "dry-run",
dryRun: sync.dryRun ?? true,
mutation: sync.mutation ?? false,
syncMode: sync.syncMode ?? null,
source,
target,
counts: sync.counts ?? null,
missing: sync.missing ?? null,
permissionFailureCount: permissionFailures.length,
permissionFailures: permissionFailures.slice(0, 4),
plannedActions: sync.plannedActions ?? null,
commands: sync.commands ?? null,
valuesPrinted: sync.valuesPrinted ?? false,
};
}
function compactMicroserviceBody(body: unknown, serviceId: string): Record<string, unknown> | null {
const record = asRecord(body);
if (record === null) return null;
@@ -388,6 +468,8 @@ function compactMicroserviceBody(body: unknown, serviceId: string): Record<strin
taskCount: record.taskCount ?? null,
schemaReady: record.schemaReady ?? null,
queue: compactQueueHealth(record.queue),
skills: compactSkillAvailability(record.skills),
skillsSync: compactSkillSync(record.skillsSync),
resourceBudget: compactRecordFields(asRecord(record.resourceBudget), [
"targetMemoryMb",
"mgrPoolMax",
@@ -401,7 +483,8 @@ function compactMicroserviceBody(body: unknown, serviceId: string): Record<strin
devReady: devReady === null ? null : {
ok: devReady.ok ?? null,
missingTools: compactStringList(devReady.missingTools, 8),
skills: devReady.skills ?? null,
skills: compactSkillAvailability(devReady.skills),
skillsSync: compactSkillSync(devReady.skillsSync),
},
executionDiagnostics: diagnostics === null ? null : {
state: diagnostics.state ?? null,
@@ -416,7 +499,7 @@ function compactMicroserviceBody(body: unknown, serviceId: string): Record<strin
};
}
function summarizeMicroserviceObservation(action: string, serviceId: string, response: unknown, args: string[]): unknown {
export function summarizeMicroserviceObservation(action: string, serviceId: string, response: unknown, args: string[]): unknown {
if (hasFlag(args, "--full") || hasFlag(args, "--raw")) return response;
const record = asRecord(response);
if (record === null) return response;