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
+4 -1
View File
@@ -191,9 +191,12 @@ D601 Code Queue runner 的长期 skills source of truth 是宿主 `/home/ubuntu/
执行面 `/health``/api/dev-ready``/api/runtime-preflight` 必须输出同一份只读 skill availability report。稳定字段包括 `source``target``requiredSkills``missingSkills``degraded``blocker``valuesPrinted=false``requiredSkills` 至少覆盖 `docs-spec``cli-spec``frontend-design``playwright-cli`;如果目标目录缺失、不是只读挂载、必需 skill 缺失,或拼写错误路径存在,报告必须显示 `ok=false` 和结构化 `blocker`,不能把 runner 能力缺口伪装成业务任务失败。
受控加载路径是更新宿主 `/home/ubuntu/.agents/skills` 后让 Code Queue Pod 通过 hostPath 读取;在线热更新只能作为临时 runbook,长期验收必须以 manifest/source-of-truth、结构化 health/preflight 和合同测试为准。需要验证时优先运行:
执行面还必须提供 dry-run skills sync/preflight 合同:稳定入口是 `GET /api/skills-sync?dryRun=1`CLI 入口是 `bun scripts/cli.ts codex skills-sync --dry-run [--full]`。该合同只描述受控 hostPath 生命周期,不复制文件、不从任意路径静默加载、不重启服务、不 rollout Pod、不读取 Secret。默认输出保持紧凑,必须报告 source、target、expected env/mount、required skill 列表、source/target skill counts、missing source/target skills、permission failure count、plannedActions 和修复指令;逐 skill 细节、完整 permission failure 和原始报告只能通过 `--full` 显式展开。非 dry-run 请求必须失败。
受控生命周期是更新宿主 `/home/ubuntu/.agents/skills` 这一 approved source,然后让生产和 dev Code Queue Pod 通过 manifest 中的 read-only hostPath 挂载读取 `/root/.agents/skills`。在线热更新只能作为临时恢复手段,长期验收必须以 manifest/source-of-truth、dry-run sync contract、结构化 health/preflight 和合同测试为准。需要验证时优先运行:
```bash
bun scripts/cli.ts codex skills-sync --dry-run
bun scripts/code-queue-runner-skills-contract-test.ts
bun scripts/cli.ts codex pr-preflight --remote --issue <issue-number>
```
@@ -1,5 +1,7 @@
import { readFileSync } from "node:fs";
import { collectSkillAvailability } from "../src/components/microservices/code-queue/src/skill-availability";
import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability";
import { codexPrPreflightQueryForTest } from "./src/code-queue";
import { summarizeMicroserviceObservation } from "./src/microservices";
type JsonRecord = Record<string, unknown>;
@@ -21,6 +23,9 @@ const devManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3
const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8");
const indexSource = readFileSync("src/components/microservices/code-queue/src/index.ts", "utf8");
const skillModule = readFileSync("src/components/microservices/code-queue/src/skill-availability.ts", "utf8");
const codeQueueCli = readFileSync("scripts/src/code-queue.ts", "utf8");
const microserviceCli = readFileSync("scripts/src/microservices.ts", "utf8");
const helpSource = readFileSync("scripts/src/help.ts", "utf8");
const promptSource = readFileSync("src/components/microservices/code-queue/src/prompts.ts", "utf8");
const docsReference = readFileSync("docs/reference/code-queue-supervision.md", "utf8");
const forbiddenPathLiteral = [".ag", "nets/skills"].join("");
@@ -69,17 +74,142 @@ assertCondition(missing.blocker === "skills-target-missing", "missing target sho
assertCondition(missing.missingSkills.includes("docs-spec") && missing.missingSkills.includes("cli-spec"), "missing target should list required missing skills", missing);
assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false");
const syncDryRun = collectSkillSyncPreflight({
source: "/home/ubuntu/.agents/skills",
target: "/path/that/does/not/exist/for-code-queue-skills-test",
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(syncDryRun.dryRun === true && syncDryRun.mutation === false, "skills sync contract must be dry-run and non-mutating", syncDryRun);
assertCondition(syncDryRun.syncMode === "hostPath-read-only-projection", "skills sync must describe the hostPath projection lifecycle", syncDryRun);
assertCondition(syncDryRun.source.path === "/home/ubuntu/.agents/skills", "skills sync must expose source", syncDryRun.source);
assertCondition(syncDryRun.target.path === "/path/that/does/not/exist/for-code-queue-skills-test", "skills sync must expose target", syncDryRun.target);
assertCondition(syncDryRun.expected.source === "/home/ubuntu/.agents/skills", "skills sync must expose stable expected source", syncDryRun.expected);
assertCondition(syncDryRun.expected.target === "/root/.agents/skills", "skills sync must expose stable expected target", syncDryRun.expected);
assertCondition(syncDryRun.expected.env === "UNIDESK_SKILLS_PATH" && syncDryRun.expected.envValue === "/root/.agents/skills", "skills sync must expose env contract", syncDryRun.expected);
assertCondition(syncDryRun.counts.requiredSkills === 2, "skills sync must expose required skill count", syncDryRun.counts);
assertCondition(syncDryRun.counts.targetSkills === 0 && syncDryRun.counts.missingTargetSkills === 2, "skills sync must expose target counts and missing count", syncDryRun.counts);
assertCondition(syncDryRun.missing.targetSkills.includes("docs-spec") && syncDryRun.missing.targetSkills.includes("cli-spec"), "skills sync must expose missing target skills", syncDryRun.missing);
assertCondition(syncDryRun.blocker === "unapproved-target", "arbitrary target paths must be blocked before silent copying", syncDryRun);
assertCondition(syncDryRun.plannedActions.copy === false && syncDryRun.plannedActions.copyFromArbitraryPath === false, "skills sync dry-run must not plan arbitrary copy", syncDryRun.plannedActions);
assertCondition(syncDryRun.plannedActions.restartRequired === false && syncDryRun.plannedActions.readsSecrets === false, "skills sync dry-run must not require restart or read secrets", syncDryRun.plannedActions);
assertCondition(Array.isArray(syncDryRun.instructions) && syncDryRun.instructions.some((item) => item.includes("read-only hostPath projection")), "skills sync must include lifecycle instructions", syncDryRun.instructions);
assertCondition(syncDryRun.valuesPrinted === false, "skills sync must declare valuesPrinted=false", syncDryRun);
assertCondition(!JSON.stringify(syncDryRun).includes(forbiddenPathLiteral), "skills sync report must not propagate misspelled path literal");
const missingTargetSync = collectSkillSyncPreflight({ target: "/path/that/does/not/exist/for-code-queue-skills-test" });
assertCondition(missingTargetSync.blocker === "unapproved-target", "non-default target must be rejected as unapproved", missingTargetSync);
assertCondition(runtimePreflight.includes("skills: SkillAvailabilityReport"), "runtime preflight type must include skills report");
assertCondition(runtimePreflight.includes("skillsSync: SkillSyncPreflightReport"), "runtime preflight type must include skills sync report");
assertCondition(runtimePreflight.includes("collectSkillAvailability"), "runtime preflight must collect skills availability");
assertCondition(runtimePreflight.includes("skills.ok && ports.codex.ok"), "runtime preflight ok must depend on skills.ok");
assertCondition(runtimePreflight.includes("collectSkillSyncPreflight"), "runtime preflight must collect skills sync preflight");
assertCondition(runtimePreflight.includes("skills.ok && skillsSync.ok && ports.codex.ok"), "runtime preflight ok must depend on skills and skillsSync");
assertCondition(indexSource.includes("const skillsReady = skills.ok === true"), "dev-ready must gate on structured skills ok");
assertCondition(indexSource.includes("collectSkillsSyncPreflight"), "runtime index must expose skills sync preflight");
assertCondition(indexSource.includes("/api/skills-sync"), "runtime must expose a dry-run skills sync endpoint");
assertCondition(indexSource.includes("pass dryRun=1"), "skills sync endpoint must reject non-dry-run calls");
assertCondition(codeQueueCli.includes("failureKind: \"dry-run-required\""), "codex skills-sync CLI must require --dry-run with structured output");
assertCondition(codeQueueCli.includes("codex skills-sync is dry-run only; pass --dry-run"), "codex skills-sync CLI must explain the dry-run requirement");
assertCondition(codeQueueCli.includes("Code Queue skills sync dry-run could not reach the control plane"), "codex skills-sync CLI must return structured control-plane failure output");
assertCondition(codeQueueCli.includes("compact-skills-sync-control-plane-failure"), "codex skills-sync CLI must keep control-plane failure output compact");
assertCondition(codeQueueCli.includes("compactSkillsSyncStatus"), "codex CLI must compact skills sync output");
assertCondition(codeQueueCli.includes("runner-skills-blocker"), "codex preflight must classify skill lifecycle blockers");
assertCondition(microserviceCli.includes("compactSkillSync"), "microservice health summary must compact skills sync output");
assertCondition(helpSource.includes("codex skills-sync --dry-run"), "CLI help must document the skills sync dry-run command");
assertCondition(docsReference.includes("codex skills-sync --dry-run"), "reference docs must document the skills sync dry-run command");
const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], {
config: null,
coreFetch: () => ({
ok: true,
status: 200,
body: {
runtimePreflight: {
ok: false,
checkedAt: "2026-05-23T00:00:00.000Z",
cwd: "/workspace/unidesk",
pid: 601,
skills: missing,
skillsSync: syncDryRun,
ports: {},
pullRequestDelivery: {
ok: true,
checkedAt: "2026-05-23T00:00:00.000Z",
tools: {},
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true },
authBroker: { ok: true, configured: true, source: "auth-broker" },
credentials: {
ghTokenPresent: false,
githubTokenPresent: false,
ghHostsConfigPresent: false,
gitCredentialsPresent: false,
},
git: {
insideWorktree: true,
branch: "code-queue/issue-68-runner-skills-lifecycle",
head: "abc1234",
originMaster: "def5678",
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
home: "/root",
homeWritable: true,
knownHostsPresent: true,
privateKeyPresent: true,
},
githubContext: {
host: "github.com",
apiBaseUrl: "https://api.github.com",
repo: "pikasTech/unidesk",
issueProbeNumber: 68,
},
egress: { proxy: {} },
remote: null,
limitations: [],
risks: [],
},
},
},
}),
}), "preflight summary");
const preflight = asRecord(preflightSummary.preflight, "preflight");
const preflightSkills = asRecord(preflight.skills, "preflight.skills");
const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync");
assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "compact preflight should classify missing skills as runner-skills-blocker", preflightSummary);
assertCondition(preflightSummary.degradedReason === "unapproved-target", "compact preflight degraded reason should use the skills sync blocker first", preflightSummary);
assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "compact preflight must show skills target", preflightSkills);
assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "compact preflight must show non-mutating skills sync dry-run", preflightSkillsSync);
assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "compact preflight must show missing target count", preflightSkillsSync);
assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSync.plannedActions").copy === false, "compact preflight must show no copy action", preflightSkillsSync);
assertCondition(preflightSkillsSync.valuesPrinted === false, "compact preflight skills sync must declare valuesPrinted=false", preflightSkillsSync);
assertCondition(!JSON.stringify(preflightSkillsSync).includes(forbiddenPathLiteral), "compact preflight must not propagate misspelled path literal");
const healthSummary = asRecord(summarizeMicroserviceObservation("health", "code-queue", {
ok: true,
status: 200,
body: {
ok: false,
service: "code-queue",
skills: missing,
skillsSync: syncDryRun,
},
}, []), "microservice health summary");
const microservice = asRecord(healthSummary.microservice, "microservice");
const healthCompact = asRecord(microservice.summary, "microservice.summary");
const healthSkills = asRecord(healthCompact.skills, "microservice.summary.skills");
const healthSkillsSync = asRecord(healthCompact.skillsSync, "microservice.summary.skillsSync");
assertCondition(healthSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "compact health must show skills target", healthSkills);
assertCondition(healthSkillsSync.dryRun === true && healthSkillsSync.mutation === false, "compact health must show dry-run skills sync", healthSkillsSync);
assertCondition(asRecord(healthSkillsSync.counts, "microservice.summary.skillsSync.counts").missingTargetSkills === 2, "compact health must show missing target count", healthSkillsSync);
assertCondition(asRecord(healthSkillsSync.plannedActions, "microservice.summary.skillsSync.plannedActions").copyFromArbitraryPath === false, "compact health must show arbitrary copy is blocked", healthSkillsSync);
assertCondition(!JSON.stringify(healthSkillsSync).includes(forbiddenPathLiteral), "compact health must not propagate misspelled path literal");
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills",
"skill availability report exposes source, target, requiredSkills, missingSkills, degraded/blocker and valuesPrinted=false",
"runtime-preflight and dev-ready use the same structured skill report",
"skills sync dry-run reports source, target, counts, missing skills, permission failures, instructions and no-copy actions",
"runtime-preflight, dev-ready, health and PR preflight use the same structured skill and sync reports",
"default health/preflight summaries expose bounded skills lifecycle evidence and --full expansion",
"misspelled skills paths are only surfaced as a forbidden diagnostic risk",
],
observedRunner: {
@@ -87,6 +217,8 @@ process.stdout.write(`${JSON.stringify({
target: available.target,
ok: available.ok,
missingSkills: available.missingSkills,
syncDryRunOk: syncDryRun.ok,
syncDryRunBlocker: syncDryRun.blocker,
valuesPrinted: available.valuesPrinted,
},
}, null, 2)}\n`);
+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;
@@ -134,7 +134,7 @@ import {
readOaTraceStepsForTask,
} from "./oa-events";
import { collectRuntimePreflight, runtimePreflightJson } from "./runtime-preflight";
import { collectSkillAvailability, skillAvailabilityJson } from "./skill-availability";
import { collectSkillAvailability, collectSkillSyncPreflight, skillAvailabilityJson, skillSyncPreflightJson } from "./skill-availability";
import { configureSelfTests, runJudgeInfraSelfTest, runQueueClaimMoveSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest, runTraceSummaryContractSelfTest } from "./self-tests";
import {
codexToolLifecycleStartedBeforeIn,
@@ -2425,6 +2425,10 @@ function collectSkillsStatus(): JsonValue {
return skillAvailabilityJson(collectSkillAvailability({ target: config.skillsPath }));
}
function collectSkillsSyncPreflight(): JsonValue {
return skillSyncPreflightJson(collectSkillSyncPreflight({ target: config.skillsPath }));
}
function collectDevReady(): JsonValue {
const now = Date.now();
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
@@ -2471,9 +2475,10 @@ function collectDevReady(): JsonValue {
const githubKnownHostProbe = runProbe("ssh-keygen", ["-F", "github.com", "-f", "/root/.ssh/known_hosts"]);
const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0;
const skills = collectSkillsStatus() as Record<string, JsonValue>;
const skillsSync = collectSkillsSyncPreflight() as Record<string, JsonValue>;
const skillsReady = skills.ok === true;
const runtimePreflight = runtimePreflightJson(collectRuntimePreflight({ includeRemote: false, includePushDryRun: false }));
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady && skillsSync.ok === true;
const value: JsonValue = {
ok,
missingTools,
@@ -2506,6 +2511,7 @@ function collectDevReady(): JsonValue {
ready: sshSharedReady,
},
skills,
skillsSync,
runtimePreflight,
};
devReadyCache = { checkedAtMs: now, value };
@@ -5195,6 +5201,7 @@ async function route(req: Request): Promise<Response> {
databaseLastError,
modelProviderConfig: codeModelProviderSourceContract(config),
skills: collectSkillsStatus(),
skillsSync: collectSkillsSyncPreflight(),
startedAt: serviceStartedAt,
}, 503);
return jsonResponse({
@@ -5213,6 +5220,7 @@ async function route(req: Request): Promise<Response> {
modelProviderConfig: codeModelProviderSourceContract(config),
executionDiagnostics: executionDiagnosticsForTasks(state.tasks),
skills: collectSkillsStatus(),
skillsSync: collectSkillsSyncPreflight(),
egressProxy: await providerGatewayEgressProxyStatus(),
oaEventPublisher: oaEventPublisherStatus(),
startedAt: serviceStartedAt,
@@ -5221,6 +5229,16 @@ async function route(req: Request): Promise<Response> {
if (url.pathname === "/logs") return jsonResponse({ ok: true, logs: recentLogs.slice(-parseLimit(url)) });
if (url.pathname === "/api/events" && req.method === "GET") return jsonResponse({ ok: false, error: "Code Queue private SSE was removed; subscribe to oa-event-flow /api/events/stream with service:code-queue tags." }, 410);
if (url.pathname === "/api/dev-ready" && req.method === "GET") return jsonResponse({ ok: true, devReady: collectDevReady() });
if (url.pathname === "/api/skills-sync" && req.method === "GET") {
const dryRun = url.searchParams.get("dryRun") === "1" || url.searchParams.get("dry-run") === "1";
if (!dryRun) return jsonResponse({
ok: false,
error: "skills sync is dry-run only from the runtime API; pass dryRun=1",
mutation: false,
valuesPrinted: false,
}, 400);
return jsonResponse({ ok: true, skillsSync: collectSkillsSyncPreflight() });
}
if (url.pathname === "/api/runtime-preflight" && req.method === "GET") {
return jsonResponse({
ok: true,
@@ -2,7 +2,7 @@
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
import { collectSkillAvailability, type SkillAvailabilityReport } from "./skill-availability";
import { collectSkillAvailability, collectSkillSyncPreflight, type SkillAvailabilityReport, type SkillSyncPreflightReport } from "./skill-availability";
export type RuntimePreflightAgentPort = "codex" | "opencode";
@@ -53,6 +53,7 @@ export interface RuntimePreflightReport {
};
path: string;
skills: SkillAvailabilityReport;
skillsSync: SkillSyncPreflightReport;
ports: Record<RuntimePreflightAgentPort, RuntimePreflightPortStatus>;
pullRequestDelivery: PullRequestDeliveryPreflight;
}
@@ -648,13 +649,14 @@ export function collectRuntimePreflight(options: RuntimePreflightOptions = {}):
const checkedAt = new Date().toISOString();
const path = process.env.PATH ?? "";
const skills = collectSkillAvailability({ target: process.env.UNIDESK_SKILLS_PATH || "/root/.agents/skills" });
const skillsSync = collectSkillSyncPreflight({ target: process.env.UNIDESK_SKILLS_PATH || "/root/.agents/skills" });
const ports = {
codex: codexStatus(path, checkedAt),
opencode: opencodeStatus(path, checkedAt),
};
const pullRequestDelivery = collectPullRequestDeliveryPreflight(options, checkedAt);
return {
ok: skills.ok && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
ok: skills.ok && skillsSync.ok && ports.codex.ok && ports.opencode.ok && pullRequestDelivery.ok,
checkedAt,
cwd: process.cwd(),
pid: process.pid,
@@ -665,6 +667,7 @@ export function collectRuntimePreflight(options: RuntimePreflightOptions = {}):
},
path,
skills,
skillsSync,
ports,
pullRequestDelivery,
};
@@ -1,4 +1,4 @@
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
@@ -38,9 +38,95 @@ export interface SkillAvailabilityReport {
valuesPrinted: false;
}
const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
const defaultSource = "/home/ubuntu/.agents/skills";
const expectedTarget = "/root/.agents/skills";
export interface SkillSyncPathReport {
path: string;
approved: boolean;
exists: boolean;
directory: boolean;
readable: boolean;
writable: boolean;
readonly: boolean;
mountPoint: string | null;
skillCount: number;
requiredSkills: string[];
missingSkills: string[];
skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
error: string | null;
}
export interface SkillSyncPreflightOptions {
source?: string;
target?: string;
requiredSkills?: string[];
}
export interface SkillSyncPreflightReport {
ok: boolean;
degraded: boolean;
blocker:
| "unapproved-source"
| "unapproved-target"
| "skills-source-missing"
| "skills-source-not-directory"
| "skills-source-permission-failed"
| "skills-target-missing"
| "skills-target-not-directory"
| "skills-target-permission-failed"
| "skills-target-not-readonly"
| "required-source-skills-missing"
| "required-target-skills-missing"
| "forbidden-skills-path-present"
| null;
checkedAt: string;
mode: "dry-run";
dryRun: true;
mutation: false;
syncMode: "hostPath-read-only-projection";
source: SkillSyncPathReport;
target: SkillSyncPathReport;
expected: {
source: string;
target: string;
env: "UNIDESK_SKILLS_PATH";
envValue: string;
mount: string;
requiredSkills: string[];
};
counts: {
sourceSkills: number;
targetSkills: number;
requiredSkills: number;
missingSourceSkills: number;
missingTargetSkills: number;
};
missing: {
sourceSkills: string[];
targetSkills: string[];
};
permissionFailures: Array<{ path: string; operation: string; error: string }>;
pathSpelling: SkillAvailabilityReport["pathSpelling"];
plannedActions: {
copy: false;
writesSource: false;
writesTarget: false;
restartRequired: false;
readsSecrets: false;
copyFromArbitraryPath: false;
};
instructions: string[];
commands: {
dryRun: string;
full: string;
health: string;
runtimePreflight: string;
contractTest: string;
};
valuesPrinted: false;
}
export const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
export const defaultSource = "/home/ubuntu/.agents/skills";
export const expectedTarget = "/root/.agents/skills";
const forbiddenSkillsDirName = [".ag", "nets"].join("");
const forbiddenTargets = [`/root/${forbiddenSkillsDirName}/skills`, `/home/ubuntu/${forbiddenSkillsDirName}/skills`];
const skillDirectoryAliases: Record<string, string[]> = {
@@ -103,6 +189,90 @@ function skillStatus(target: string, requiredSkills: string[]): SkillAvailabilit
});
}
function accessProbe(path: string, mode: number, operation: string): { ok: boolean; failure: { path: string; operation: string; error: string } | null } {
try {
accessSync(path, mode);
return { ok: true, failure: null };
} catch (error) {
return {
ok: false,
failure: {
path,
operation,
error: error instanceof Error ? error.message : String(error),
},
};
}
}
function emptySkillStatuses(target: string, requiredSkills: string[]): SkillAvailabilityReport["skills"] {
return requiredSkills.map((name) => ({
name,
present: false,
skillMdPresent: false,
path: resolve(target, name),
}));
}
function collectSyncPathReport(path: string, approved: boolean, requiredSkills: string[]): { report: SkillSyncPathReport; permissionFailures: SkillSyncPreflightReport["permissionFailures"] } {
const mountInfo = mountInfoForPath(path);
const exists = existsSync(path);
const permissionFailures: SkillSyncPreflightReport["permissionFailures"] = [];
let directory = false;
let readable = false;
let writable = false;
let skillCount = 0;
let skills = emptySkillStatuses(path, requiredSkills);
let error: string | null = null;
if (exists) {
try {
const stat = statSync(path);
directory = stat.isDirectory();
} catch (probeError) {
error = probeError instanceof Error ? probeError.message : String(probeError);
}
}
if (exists && directory) {
const read = accessProbe(path, constants.R_OK | constants.X_OK, "read-directory");
readable = read.ok;
if (read.failure !== null) permissionFailures.push(read.failure);
const write = accessProbe(path, constants.W_OK, "write-check");
writable = write.ok;
if (readable) {
try {
const entries = readdirSync(path, { withFileTypes: true });
skillCount = entries.filter((entry) => entry.isDirectory()).length;
skills = skillStatus(path, requiredSkills);
} catch (probeError) {
error = probeError instanceof Error ? probeError.message : String(probeError);
permissionFailures.push({ path, operation: "list-skills", error });
}
}
}
const missingSkills = skills.filter((skill) => !skill.present || !skill.skillMdPresent).map((skill) => skill.name);
return {
report: {
path,
approved,
exists,
directory,
readable,
writable,
readonly: exists && directory && (mountInfo.readonly === true || !writable),
mountPoint: mountInfo.mountPoint,
skillCount,
requiredSkills,
missingSkills,
skills,
error,
},
permissionFailures,
};
}
export function collectSkillAvailability(options: SkillAvailabilityOptions): SkillAvailabilityReport {
const target = options.target;
const source = options.source ?? defaultSource;
@@ -183,6 +353,107 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
};
}
export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {}): SkillSyncPreflightReport {
const sourcePath = options.source ?? defaultSource;
const targetPath = options.target ?? expectedTarget;
const requiredSkills = options.requiredSkills ?? defaultRequiredSkills;
const checkedAt = new Date().toISOString();
const source = collectSyncPathReport(sourcePath, sourcePath === defaultSource, requiredSkills);
const target = collectSyncPathReport(targetPath, targetPath === expectedTarget, requiredSkills);
const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
const permissionFailures = [...source.permissionFailures, ...target.permissionFailures];
const blocker = !source.report.approved
? "unapproved-source"
: !target.report.approved
? "unapproved-target"
: !source.report.exists
? "skills-source-missing"
: !source.report.directory
? "skills-source-not-directory"
: !source.report.readable
? "skills-source-permission-failed"
: !target.report.exists
? "skills-target-missing"
: !target.report.directory
? "skills-target-not-directory"
: !target.report.readable
? "skills-target-permission-failed"
: !target.report.readonly
? "skills-target-not-readonly"
: source.report.missingSkills.length > 0
? "required-source-skills-missing"
: target.report.missingSkills.length > 0
? "required-target-skills-missing"
: forbiddenPathExists
? "forbidden-skills-path-present"
: null;
const ok = blocker === null;
return {
ok,
degraded: !ok,
blocker,
checkedAt,
mode: "dry-run",
dryRun: true,
mutation: false,
syncMode: "hostPath-read-only-projection",
source: source.report,
target: target.report,
expected: {
source: defaultSource,
target: expectedTarget,
env: "UNIDESK_SKILLS_PATH",
envValue: expectedTarget,
mount: `${defaultSource} mounted read-only to ${expectedTarget}`,
requiredSkills,
},
counts: {
sourceSkills: source.report.skillCount,
targetSkills: target.report.skillCount,
requiredSkills: requiredSkills.length,
missingSourceSkills: source.report.missingSkills.length,
missingTargetSkills: target.report.missingSkills.length,
},
missing: {
sourceSkills: source.report.missingSkills,
targetSkills: target.report.missingSkills,
},
permissionFailures,
pathSpelling: {
expectedTarget,
forbiddenPathChecked: true,
forbiddenPathExists,
forbiddenPathMustNotBeUsed: true,
},
plannedActions: {
copy: false,
writesSource: false,
writesTarget: false,
restartRequired: false,
readsSecrets: false,
copyFromArbitraryPath: false,
},
instructions: [
`Use ${defaultSource} as the only approved runner skills source and ${expectedTarget} as the only approved Code Queue container target.`,
"Keep the lifecycle as a read-only hostPath projection; this dry-run does not copy files, restart services, roll pods, or read secrets.",
"Update the approved source through the controlled skills installation path, then let production and dev Code Queue pods read it through the manifest mount.",
"Use the full command only when per-skill status or permission failure detail is needed.",
],
commands: {
dryRun: "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",
runtimePreflight: "bun scripts/cli.ts codex pr-preflight --remote",
contractTest: "bun scripts/code-queue-runner-skills-contract-test.ts",
},
valuesPrinted: false,
};
}
export function skillAvailabilityJson(report: SkillAvailabilityReport): JsonValue {
return report as unknown as JsonValue;
}
export function skillSyncPreflightJson(report: SkillSyncPreflightReport): JsonValue {
return report as unknown as JsonValue;
}