fix: expose code queue runner skills contract
This commit is contained in:
@@ -242,6 +242,8 @@ D601 Code Queue runner 的长期 skills source of truth 是宿主 `/home/ubuntu/
|
||||
|
||||
执行面 `/health`、`/api/dev-ready` 和 `/api/runtime-preflight` 必须输出同一份只读 skill availability report。稳定字段包括 `source`、`target`、`resolvedPath`、`resolvedPathSource`、`resolution`、`requiredSkills`、`missingSkills`、`sourceSkillCount`、`targetSkillCount`、`degraded`、`degradedReason`、`blocker`、`pathSpelling` 和 `valuesPrinted=false`。`requiredSkills` 至少覆盖 `docs-spec`、`cli-spec`、`frontend-design` 与 `playwright-cli`;如果 source 与 target 都缺失、必需 skill 均不可用、拼写错误路径被配置或拼写错误路径已存在,报告必须显示 `ok=false` 和结构化 `blocker`,不能把 runner 能力缺口伪装成业务任务失败。target symlink 到 approved source 属于可接受的兼容修复,必须显示 `resolvedPathSource=target-symlink` 和有界 skill count。
|
||||
|
||||
`codex pr-preflight --remote` 默认视图必须保留同一份可操作 `skillsContract`,不能只给出模糊的 `skills-target-missing`。稳定字段包括 `source`、`target`、`resolvedPath`、`resolvedPathSource`、`requiredSkills`、`missingSkills`、`sourceSkillCount`、`targetSkillCount`、`sourceMissingSkills`、`targetMissingSkills`、`hostRolloutRequired`、`degradedReason`、`blocker`、`repairHint`、`pathSpelling` 和 `valuesPrinted=false`。当 `/api/runtime-preflight` 的旧 `skills` report 缺少 `resolution` 或 counts,但 `skillsSync` 已证明 approved source 可读且 target 未投影时,CLI 必须从 `skillsSync` 合成同一份 `skillsContract`,并显示 `hostRolloutRequired=true` 与修复指令。`--full` 或 `--raw` 可以展开完整 `skills`、`skillsSync`、tool 和 transport 观测;默认输出仍保持低噪声,只保留指挥官能直接判断和恢复的字段。
|
||||
|
||||
执行面还必须提供 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`;provider dev container 还必须通过启动脚本把同一 source bind 到同一 target。在线热更新只能作为临时恢复手段,长期验收必须以 manifest/source-of-truth、runner container bind、dry-run sync contract、结构化 health/preflight 和合同测试为准。需要验证时优先运行:
|
||||
|
||||
@@ -405,6 +405,12 @@ const skillsPreflightTransport = {
|
||||
const defaultPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], skillsPreflightTransport), "default preflight summary");
|
||||
assertCondition(defaultPreflightSummary.failureKind === "runner-skills-blocker", "missing target should classify as runner skills blocker even when source exists", defaultPreflightSummary);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").hostRolloutRequired === true, "default preflight should expose host rollout blocker separately", defaultPreflightSummary);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").source === missing.source, "default preflight should expose skills source in the bounded contract", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").target === missing.target, "default preflight should expose skills target in the bounded contract", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(Array.isArray(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").requiredSkills), "default preflight should expose requiredSkills in the bounded contract", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(Array.isArray(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").missingSkills), "default preflight should expose missingSkills in the bounded contract", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").repairHint !== null, "default preflight should expose repairHint in the bounded contract", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").valuesPrinted === false, "default preflight skills contract must declare valuesPrinted=false", defaultPreflightSummary.skillsContract);
|
||||
assertCondition(defaultPreflightSummary.preflight === undefined, "default PR preflight should omit detailed preflight internals", defaultPreflightSummary);
|
||||
assertCondition(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").fullDetailOmitted === true, "default PR preflight should disclose full detail omission", defaultPreflightSummary.disclosure);
|
||||
assertCondition(String(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").expandWith ?? "").includes("--full"), "default PR preflight should point to --full expansion", defaultPreflightSummary.disclosure);
|
||||
@@ -424,6 +430,190 @@ assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSy
|
||||
assertCondition(preflightSkillsSync.valuesPrinted === false, "full preflight skills sync must declare valuesPrinted=false", preflightSkillsSync);
|
||||
assertCondition(!JSON.stringify(preflightSkillsSync).includes(forbiddenPathLiteral), "full preflight must not propagate misspelled path literal");
|
||||
|
||||
const legacyRuntimeSkills = {
|
||||
ok: false,
|
||||
runnerUsable: false,
|
||||
contractOk: false,
|
||||
path: "/root/.agents/skills",
|
||||
resolvedPath: "/root/.agents/skills",
|
||||
resolvedPathSource: null,
|
||||
resolution: null,
|
||||
source: "/home/ubuntu/.agents/skills",
|
||||
target: "/root/.agents/skills",
|
||||
exists: false,
|
||||
available: false,
|
||||
degraded: true,
|
||||
blocker: "skills-target-missing",
|
||||
degradedReason: "skills-target-missing",
|
||||
readonly: false,
|
||||
skillCount: 0,
|
||||
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
missingSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
valuesPrinted: false,
|
||||
pathSpelling: {
|
||||
expectedTarget: "/root/.agents/skills",
|
||||
forbiddenPathChecked: true,
|
||||
forbiddenPathExists: false,
|
||||
forbiddenPathConfigured: false,
|
||||
forbiddenPathRoles: [],
|
||||
forbiddenPathMustNotBeUsed: true,
|
||||
},
|
||||
repairHint: "Mount /home/ubuntu/.agents/skills read-only at /root/.agents/skills, set UNIDESK_SKILLS_PATH=/root/.agents/skills, and remove any forbidden skills path spelling.",
|
||||
};
|
||||
const legacyRuntimeSkillsSync = {
|
||||
ok: false,
|
||||
degraded: true,
|
||||
blocker: "skills-target-missing",
|
||||
checkedAt: "2026-05-23T00:00:00.000Z",
|
||||
mode: "dry-run",
|
||||
dryRun: true,
|
||||
mutation: false,
|
||||
syncMode: "hostPath-read-only-projection",
|
||||
source: {
|
||||
path: "/home/ubuntu/.agents/skills",
|
||||
approved: true,
|
||||
exists: true,
|
||||
directory: true,
|
||||
readable: true,
|
||||
writable: true,
|
||||
readonly: false,
|
||||
mountPoint: "/home/ubuntu",
|
||||
symlink: false,
|
||||
realPath: null,
|
||||
skillCount: 49,
|
||||
version: null,
|
||||
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
missingSkills: [],
|
||||
error: null,
|
||||
},
|
||||
target: {
|
||||
path: "/root/.agents/skills",
|
||||
approved: true,
|
||||
exists: false,
|
||||
directory: false,
|
||||
readable: false,
|
||||
writable: false,
|
||||
readonly: false,
|
||||
mountPoint: "/",
|
||||
symlink: false,
|
||||
realPath: null,
|
||||
skillCount: 0,
|
||||
version: null,
|
||||
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
missingSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
error: null,
|
||||
},
|
||||
expected: {
|
||||
source: "/home/ubuntu/.agents/skills",
|
||||
target: "/root/.agents/skills",
|
||||
env: "UNIDESK_SKILLS_PATH",
|
||||
envValue: "/root/.agents/skills",
|
||||
mount: "/home/ubuntu/.agents/skills mounted read-only to /root/.agents/skills",
|
||||
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
},
|
||||
counts: {
|
||||
sourceSkills: 49,
|
||||
targetSkills: 0,
|
||||
requiredSkills: 4,
|
||||
missingSourceSkills: 0,
|
||||
missingTargetSkills: 4,
|
||||
},
|
||||
version: null,
|
||||
missing: {
|
||||
sourceSkills: [],
|
||||
targetSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
||||
},
|
||||
permissionFailures: [],
|
||||
pathSpelling: {
|
||||
expectedTarget: "/root/.agents/skills",
|
||||
forbiddenPathChecked: true,
|
||||
forbiddenPathExists: false,
|
||||
forbiddenPathConfigured: false,
|
||||
forbiddenPathRoles: [],
|
||||
forbiddenPathMustNotBeUsed: true,
|
||||
},
|
||||
plannedActions: {
|
||||
copy: false,
|
||||
writesSource: false,
|
||||
writesTarget: false,
|
||||
restartRequired: false,
|
||||
readsSecrets: false,
|
||||
copyFromArbitraryPath: false,
|
||||
},
|
||||
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,
|
||||
};
|
||||
const legacyRuntimePreflightTransport = {
|
||||
config: null,
|
||||
coreFetch: () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
runtimePreflight: {
|
||||
ok: false,
|
||||
checkedAt: "2026-05-23T00:00:00.000Z",
|
||||
cwd: "/workspace/unidesk",
|
||||
pid: 601,
|
||||
skills: legacyRuntimeSkills,
|
||||
skillsSync: legacyRuntimeSkillsSync,
|
||||
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: true,
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
const legacyDefaultPreflight = asRecord(codexPrPreflightQueryForTest(["--remote"], legacyRuntimePreflightTransport), "legacy default preflight");
|
||||
const legacySkillsContract = asRecord(legacyDefaultPreflight.skillsContract, "legacyDefaultPreflight.skillsContract");
|
||||
assertCondition(legacyDefaultPreflight.failureKind === "runner-skills-blocker", "legacy runtime shape should still classify missing target as runner skills blocker", legacyDefaultPreflight);
|
||||
assertCondition(legacySkillsContract.source === "/home/ubuntu/.agents/skills", "legacy runtime shape should expose source path from skillsSync", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.target === "/root/.agents/skills", "legacy runtime shape should expose target path from skillsSync", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.hostRolloutRequired === true, "legacy runtime shape with source available and target missing should require host rollout", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.degradedReason === "skills-target-missing", "legacy runtime shape should keep actionable degraded reason", legacySkillsContract);
|
||||
assertCondition(Array.isArray(legacySkillsContract.requiredSkills) && legacySkillsContract.requiredSkills.includes("docs-spec"), "legacy runtime shape should expose requiredSkills", legacySkillsContract);
|
||||
assertCondition(Array.isArray(legacySkillsContract.missingSkills) && legacySkillsContract.missingSkills.includes("docs-spec"), "legacy runtime shape should expose missingSkills", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.sourceSkillCount === 49 && legacySkillsContract.targetSkillCount === 0, "legacy runtime shape should expose source/target skill counts", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.repairHint !== null, "legacy runtime shape should expose repairHint", legacySkillsContract);
|
||||
assertCondition(legacySkillsContract.valuesPrinted === false, "legacy runtime shape contract must declare valuesPrinted=false", legacySkillsContract);
|
||||
|
||||
const typoPreflightTransport = {
|
||||
config: null,
|
||||
coreFetch: () => ({
|
||||
|
||||
+130
-4
@@ -6382,6 +6382,118 @@ function compactSkillsSyncStatus(value: unknown, full = false): Record<string, u
|
||||
return compact;
|
||||
}
|
||||
|
||||
function compactStringArray(value: unknown): string[] {
|
||||
return Array.from(new Set(asArray(value).map((item) => String(item ?? "")).filter((item) => item.length > 0)));
|
||||
}
|
||||
|
||||
function firstStringField(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.length > 0) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function firstNumberField(...values: unknown[]): number | null {
|
||||
for (const value of values) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeStringArrays(...values: unknown[]): string[] {
|
||||
return Array.from(new Set(values.flatMap((value) => compactStringArray(value))));
|
||||
}
|
||||
|
||||
function compactPrPreflightSkillsContract(
|
||||
skills: Record<string, unknown> | null,
|
||||
skillsSync: Record<string, unknown> | null,
|
||||
existing: Record<string, unknown> | null = null,
|
||||
): Record<string, unknown> | null {
|
||||
if (skills === null && skillsSync === null && existing === null) return null;
|
||||
const resolution = asRecord(skills?.resolution);
|
||||
const sourceReport = asRecord(skillsSync?.source);
|
||||
const targetReport = asRecord(skillsSync?.target);
|
||||
const expected = asRecord(skillsSync?.expected);
|
||||
const counts = asRecord(skillsSync?.counts);
|
||||
const missing = asRecord(skillsSync?.missing);
|
||||
const existingResolution = asRecord(existing?.resolution);
|
||||
const source = firstStringField(existing?.source, skills?.source, sourceReport?.path, expected?.source);
|
||||
const target = firstStringField(existing?.target, skills?.target, targetReport?.path, expected?.target, skills?.path, skills?.resolvedPath);
|
||||
const resolvedPath = firstStringField(existing?.resolvedPath, skills?.resolvedPath, skills?.path, resolution?.resolvedPath, target);
|
||||
const sourceMissingSkills = mergeStringArrays(existing?.sourceMissingSkills, skills?.sourceMissingSkills, missing?.sourceSkills, sourceReport?.missingSkills);
|
||||
const targetMissingSkills = mergeStringArrays(existing?.targetMissingSkills, skills?.targetMissingSkills, missing?.targetSkills, targetReport?.missingSkills);
|
||||
const requiredSkills = mergeStringArrays(existing?.requiredSkills, skills?.requiredSkills, expected?.requiredSkills, sourceReport?.requiredSkills, targetReport?.requiredSkills);
|
||||
const missingSkills = mergeStringArrays(existing?.missingSkills, skills?.missingSkills, targetMissingSkills);
|
||||
const sourceSkillCount = firstNumberField(existing?.sourceSkillCount, skills?.sourceSkillCount, counts?.sourceSkills, sourceReport?.skillCount);
|
||||
const targetSkillCount = firstNumberField(existing?.targetSkillCount, skills?.targetSkillCount, counts?.targetSkills, targetReport?.skillCount, skills?.skillCount);
|
||||
const blocker = firstStringField(existing?.degradedReason, existing?.blocker, skills?.degradedReason, skills?.blocker, skillsSync?.blocker);
|
||||
const contractOk = existing?.ok === true || skills?.contractOk === true || skills?.runnerUsable === true || skills?.ok === true && skillsSync?.ok !== false || skillsSync?.ok === true && skills === null;
|
||||
const degraded = !contractOk;
|
||||
const forbiddenPathBlocker = blocker === "forbidden-skills-path-configured" || blocker === "forbidden-skills-path-present";
|
||||
const sourceHasRequiredSkills = sourceMissingSkills.length === 0
|
||||
&& (sourceReport?.exists === true && sourceReport?.readable === true || typeof sourceSkillCount === "number" && sourceSkillCount > 0);
|
||||
const targetProjectionMissing = !forbiddenPathBlocker
|
||||
&& (
|
||||
blocker === "skills-target-missing"
|
||||
|| blocker === "skills-target-not-directory"
|
||||
|| blocker === "skills-target-permission-failed"
|
||||
|| blocker === "skills-target-not-readonly"
|
||||
|| blocker === "required-target-skills-missing"
|
||||
|| targetMissingSkills.length > 0
|
||||
|| targetReport?.exists === false
|
||||
|| skills?.resolvedPathSource === "missing"
|
||||
);
|
||||
const hostRolloutRequired = existing?.hostRolloutRequired === true
|
||||
|| existingResolution?.hostRolloutRequired === true
|
||||
|| resolution?.hostRolloutRequired === true
|
||||
|| degraded && sourceHasRequiredSkills && targetProjectionMissing;
|
||||
const resolvedPathSource = firstStringField(existing?.resolvedPathSource, skills?.resolvedPathSource, resolution?.resolvedPathSource)
|
||||
?? (targetProjectionMissing ? "missing" : null);
|
||||
const pathSpelling = compactObjectFields(asRecord(existing?.pathSpelling) ?? asRecord(skills?.pathSpelling) ?? asRecord(skillsSync?.pathSpelling), [
|
||||
"expectedTarget",
|
||||
"forbiddenPathChecked",
|
||||
"forbiddenPathExists",
|
||||
"forbiddenPathConfigured",
|
||||
"forbiddenPathRoles",
|
||||
"forbiddenPathMustNotBeUsed",
|
||||
]);
|
||||
const repairHint = firstStringField(existing?.repairHint, skills?.repairHint)
|
||||
?? (degraded
|
||||
? hostRolloutRequired && source !== null && target !== null
|
||||
? `Source ${source} has the required skills; restore the read-only ${source} -> ${target} projection before starting runners.`
|
||||
: source !== null && target !== null
|
||||
? `Mount ${source} read-only at ${target}, set UNIDESK_SKILLS_PATH=${target}, and remove any forbidden skills path spelling.`
|
||||
: "Restore the approved runner skills source/target projection before starting runners."
|
||||
: null);
|
||||
return {
|
||||
ok: contractOk,
|
||||
degraded,
|
||||
source,
|
||||
target,
|
||||
resolvedPath,
|
||||
resolvedPathSource,
|
||||
hostRolloutRequired,
|
||||
degradedReason: blocker,
|
||||
blocker,
|
||||
requiredSkills,
|
||||
missingSkills,
|
||||
sourceSkillCount,
|
||||
targetSkillCount,
|
||||
sourceMissingSkills,
|
||||
targetMissingSkills,
|
||||
sourceHasRequiredSkills,
|
||||
targetProjected: !targetProjectionMissing && !degraded,
|
||||
repairHint,
|
||||
valuesPrinted: false,
|
||||
...(pathSpelling === null ? {} : { pathSpelling }),
|
||||
note: degraded
|
||||
? hostRolloutRequired
|
||||
? "runner skills source is available, but the read-only target projection is missing and needs controlled host rollout repair"
|
||||
: "runner skills target projection contract is degraded; use repairHint before starting runners"
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueueDevReady(): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/dev-ready")));
|
||||
const devReady = asRecord(response.body.devReady) ?? {};
|
||||
@@ -6903,7 +7015,19 @@ function compactRecommendedActions(record: Record<string, unknown>): Record<stri
|
||||
}
|
||||
|
||||
function compactPrPreflightCommanderView(record: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> {
|
||||
if (options.full) return record;
|
||||
const preflight = asRecord(record.preflight);
|
||||
const normalizedSkillsContract = compactPrPreflightSkillsContract(
|
||||
compactSkillsStatus(record.skills ?? preflight?.skills),
|
||||
compactSkillsSyncStatus(record.skillsSync ?? preflight?.skillsSync, options.full),
|
||||
asRecord(record.skillsContract) ?? asRecord(preflight?.skillsContract),
|
||||
);
|
||||
if (options.full) {
|
||||
return normalizedSkillsContract === null ? record : {
|
||||
...record,
|
||||
skillsContract: normalizedSkillsContract,
|
||||
...(preflight === null ? {} : { preflight: { ...preflight, skillsContract: normalizedSkillsContract } }),
|
||||
};
|
||||
}
|
||||
|
||||
const tokenCoverage = prPreflightTokenCoverage(record);
|
||||
const authBroker = prPreflightAuthBroker(record);
|
||||
@@ -6969,7 +7093,7 @@ function compactPrPreflightCommanderView(record: Record<string, unknown>, option
|
||||
capabilitySource: authBrokerCapability?.source ?? tokenCoverage?.credentialSource ?? null,
|
||||
},
|
||||
},
|
||||
skillsContract: record.skillsContract ?? undefined,
|
||||
skillsContract: normalizedSkillsContract ?? undefined,
|
||||
activeRunnerPrCapability: {
|
||||
scope: activeRunnerDevContainer.scope ?? "current-cli-process",
|
||||
tokenCandidatePresent: activeRunnerTokenCandidatePresent,
|
||||
@@ -7329,6 +7453,7 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
const githubTransient = githubTransientEvidence(pull);
|
||||
const skillsBlocked = skills !== null && skills.runnerUsable !== true && skills.ok !== true;
|
||||
const skillsContractDegraded = skills !== null && skills.contractOk !== true;
|
||||
const skillsContract = compactPrPreflightSkillsContract(skills, skillsSync);
|
||||
const failureKind = !tokenCoverage.ok
|
||||
? "auth-missing"
|
||||
: skillsBlocked
|
||||
@@ -7375,12 +7500,13 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
},
|
||||
skills,
|
||||
skillsSync,
|
||||
skillsContract: {
|
||||
skillsContract: skillsContract ?? {
|
||||
ok: !skillsContractDegraded,
|
||||
degraded: skillsContractDegraded,
|
||||
hostRolloutRequired: asRecord(skills?.resolution)?.hostRolloutRequired === true,
|
||||
degradedReason: skills?.degradedReason ?? skills?.blocker ?? skillsSync?.blocker ?? null,
|
||||
note: skillsContractDegraded ? "runner skills are usable through the resolved path, but the read-only target projection still needs host rollout repair" : null,
|
||||
valuesPrinted: false,
|
||||
note: skillsContractDegraded ? "runner skills target projection contract is degraded; use repairHint before starting runners" : null,
|
||||
},
|
||||
tokenCoverage,
|
||||
authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli),
|
||||
|
||||
Reference in New Issue
Block a user