fix(code-queue): add runner skills sync contract
This commit is contained in:
+180
-12
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user