feat: add gh pr preflight helper

This commit is contained in:
Codex
2026-05-23 08:42:41 +00:00
parent cdaf28a5c4
commit 90ffd028de
6 changed files with 323 additions and 8 deletions
+41
View File
@@ -239,6 +239,8 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(usage.some((line) => line.includes("gh pr read")), "gh help should list pr read", { usage });
assertCondition(usage.some((line) => line.includes("gh pr view")), "gh help should list pr view", { usage });
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document pr shorthand and raw/full disclosure", { usage });
assertCondition(usage.some((line) => line.includes("gh preflight")), "gh help should list top-level preflight alias", { usage });
assertCondition(usage.some((line) => line.includes("gh pr preflight")), "gh help should list pr preflight", { usage });
assertCondition(usage.some((line) => line.includes("gh pr create")), "gh help should list pr create", { usage });
assertCondition(usage.some((line) => line.includes("gh pr edit")), "gh help should list pr edit", { usage });
assertCondition(usage.some((line) => line.includes("gh pr comment")), "gh help should list pr comment", { usage });
@@ -250,6 +252,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
assertCondition(notes.some((line) => line.includes("PR list defaults to --state all")), "gh help should document pr list default state", { notes });
assertCondition(notes.some((line) => line.includes("stateDetail") && line.includes("mergedAt")), "gh help should describe closeout field normalization", { notes });
assertCondition(notes.some((line) => line.includes("low-noise read-only closeout helper")), "gh help should document PR preflight closeout helper", { notes });
const mock = await startMockGitHub();
const env = {
@@ -367,6 +370,41 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(unsupportedReadMessage.includes("projectCards"), "unsupported pr read field message should name the bad field", unsupportedReadFieldData);
assertCondition(unsupportedReadMessage.includes("mergedAt") && unsupportedReadMessage.includes("mergeCommit"), "unsupported pr read field message should include supported closeout fields", unsupportedReadFieldData);
const closeoutPreflight = await runCli(["gh", "pr", "preflight", "42", "--repo", "pikasTech/unidesk"], env);
assertCondition(closeoutPreflight.status === 0, "pr preflight should succeed through REST and GraphQL", closeoutPreflight.json ?? { stdout: closeoutPreflight.stdout });
assertCondition(!closeoutPreflight.stdout.includes("contract-token"), "pr preflight must not print token values", { stdout: closeoutPreflight.stdout });
const closeoutPreflightData = dataOf(closeoutPreflight.json ?? {});
assertCondition(closeoutPreflightData.command === "pr preflight", "pr preflight should report command", closeoutPreflightData);
assertCondition(closeoutPreflightData.readOnly === true && closeoutPreflightData.writesRemote === false, "pr preflight must stay read-only", closeoutPreflightData);
assertCondition(!("raw" in closeoutPreflightData), "pr preflight default output should omit raw payloads", closeoutPreflightData);
const authCapability = closeoutPreflightData.authCapability as JsonRecord;
assertCondition(authCapability.ok === true && authCapability.tokenPresent === true && authCapability.tokenSource === "GH_TOKEN", "pr preflight should expose redacted auth capability", authCapability);
assertCondition(authCapability.valuesPrinted === false, "pr preflight should explicitly avoid secret values", authCapability);
const preflightPr = closeoutPreflightData.pullRequest as JsonRecord;
assertCondition(preflightPr.number === 42 && preflightPr.bodyOmitted === true, "pr preflight should return bounded PR metadata", preflightPr);
const mergeability = closeoutPreflightData.mergeability as JsonRecord;
assertCondition(mergeability.mergeable === "MERGEABLE" && mergeability.mergeStateStatus === "CLEAN", "pr preflight should expose mergeability", mergeability);
assertCondition(mergeability.readyForCommanderMerge === true && mergeability.conclusion === "ready", "pr preflight should summarize closeout readiness", mergeability);
const preflightStatus = closeoutPreflightData.statusChecks as JsonRecord;
const preflightCounts = preflightStatus.counts as JsonRecord;
assertCondition(preflightStatus.state === "SUCCESS" && preflightStatus.rawOmitted === true, "pr preflight default status rollup should be compact", preflightStatus);
assertCondition(preflightCounts.success === 2, "pr preflight should count successful contexts", preflightStatus);
const policy = closeoutPreflightData.policy as JsonRecord;
assertCondition(policy.mergesPr === false && policy.mergeCommandSupported === false && policy.unideskCliMergeSupported === false, "pr preflight policy should block UniDesk CLI merge execution", policy);
const aliasPreflight = await runCli(["gh", "preflight", "42", "--repo", "pikasTech/unidesk"], env);
assertCondition(aliasPreflight.status === 0, "top-level gh preflight alias should succeed", aliasPreflight.json ?? { stdout: aliasPreflight.stdout });
const aliasPreflightData = dataOf(aliasPreflight.json ?? {});
assertCondition(aliasPreflightData.command === "preflight", "top-level gh preflight should report alias command", aliasPreflightData);
assertCondition((aliasPreflightData.policy as JsonRecord).mergesPr === false, "top-level gh preflight alias must not merge", aliasPreflightData);
const fullPreflight = await runCli(["gh", "pr", "preflight", "42", "--repo", "pikasTech/unidesk", "--full"], env);
assertCondition(fullPreflight.status === 0, "pr preflight --full should succeed", fullPreflight.json ?? { stdout: fullPreflight.stdout });
const fullPreflightData = dataOf(fullPreflight.json ?? {});
const fullStatus = fullPreflightData.statusChecks as JsonRecord;
assertCondition(fullStatus.rawOmitted === false && Array.isArray(fullStatus.contexts), "pr preflight --full should include status contexts", fullStatus);
assertCondition(typeof fullPreflightData.raw === "object" && fullPreflightData.raw !== null, "pr preflight --full should include raw read payload summary", fullPreflightData);
const preflight = await runBun([
"scripts/code-queue-pr-preflight-example.ts",
"--repo",
@@ -558,6 +596,9 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
"GitHub DNS/API transients are retryable and distinct from auth or PR semantic failures",
"pr view closeout metadata fields are accepted and hydrated through GraphQL",
"pr read unsupported fields fail structurally with supported closeout fields listed",
"pr preflight exposes redacted auth plus compact merge/status closeout metadata",
"top-level gh preflight alias works for commander closeout",
"pr preflight --full is the explicit status-context disclosure path",
"pr create dry-run exposes planned operation",
"pr comment dry-run preserves markdown text",
"pr update/edit use low-noise REST PATCH without GraphQL projectCards",
+277 -3
View File
@@ -3901,6 +3901,264 @@ async function prGraphqlMetadata(repo: string, token: string, number: number): P
return pullRequest;
}
function statusContextName(context: GitHubPullRequestGraphqlStatusContext): string {
return context.name ?? context.context ?? "(unnamed status check)";
}
function statusContextBucket(context: GitHubPullRequestGraphqlStatusContext): string {
const type = context.__typename ?? "StatusContext";
const status = (context.status ?? "").toUpperCase();
const conclusion = (context.conclusion ?? "").toUpperCase();
const state = (context.state ?? "").toUpperCase();
if (type === "CheckRun") {
if (status.length > 0 && status !== "COMPLETED") return "pending";
if (conclusion === "SUCCESS") return "success";
if (conclusion === "NEUTRAL") return "neutral";
if (conclusion === "SKIPPED") return "skipped";
if (conclusion === "CANCELLED") return "cancelled";
if (conclusion === "TIMED_OUT") return "timed-out";
if (["FAILURE", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE"].includes(conclusion)) return "failure";
return "unknown";
}
if (state === "SUCCESS") return "success";
if (state === "PENDING" || state === "EXPECTED") return "pending";
if (state === "FAILURE" || state === "ERROR") return "failure";
return "unknown";
}
function compactStatusContext(context: GitHubPullRequestGraphqlStatusContext): Record<string, unknown> {
const bucket = statusContextBucket(context);
return {
name: statusContextName(context),
type: context.__typename ?? "StatusContext",
bucket,
status: context.status ?? null,
conclusion: context.conclusion ?? null,
state: context.state ?? null,
targetUrl: context.targetUrl ?? null,
description: context.description === undefined || context.description === null ? null : preview(context.description),
};
}
function statusRollupSummary(
repo: string,
number: number,
rollup: GitHubPullRequestGraphqlStatusCheckRollup | null | undefined,
includeRaw: boolean,
): Record<string, unknown> {
const contexts = rollup?.contexts?.nodes ?? [];
const compactContexts = contexts.map(compactStatusContext);
const counts: Record<string, number> = {
success: 0,
failure: 0,
pending: 0,
neutral: 0,
skipped: 0,
cancelled: 0,
"timed-out": 0,
unknown: 0,
};
for (const context of compactContexts) {
const bucket = String(context.bucket ?? "unknown");
counts[bucket] = (counts[bucket] ?? 0) + 1;
}
const nonSuccess = compactContexts.filter((context) => context.bucket !== "success");
const failing = compactContexts.filter((context) => context.bucket === "failure" || context.bucket === "cancelled" || context.bucket === "timed-out");
const pending = compactContexts.filter((context) => context.bucket === "pending" || context.bucket === "unknown");
const neutral = compactContexts.filter((context) => context.bucket === "neutral" || context.bucket === "skipped");
return {
state: rollup?.state ?? null,
totalContexts: compactContexts.length,
counts,
failingContexts: failing.slice(0, 10),
pendingContexts: pending.slice(0, 10),
neutralContexts: neutral.slice(0, 10),
nonSuccessPreview: nonSuccess.slice(0, 12),
omittedNonSuccessContexts: Math.max(0, nonSuccess.length - 12),
rawOmitted: !includeRaw,
...(includeRaw
? { contexts: compactContexts, raw: rollup ?? null }
: { fullHint: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo} --full` }),
};
}
function compactProbeOk(value: unknown): boolean | null {
if (value === "ok") return true;
if (value === "skipped") return null;
if (isRecord(value) && value.ok === true) return true;
if (isRecord(value) && value.ok === false) return false;
return null;
}
function compactAuthCapability(auth: GitHubCommandResult): Record<string, unknown> {
const token = isRecord(auth.token) ? auth.token : {};
const gh = isRecord(auth.gh) ? auth.gh : {};
const probes = isRecord(auth.probes) ? auth.probes : {};
return {
ok: auth.ok === true,
degradedReason: auth.ok === false ? auth.degradedReason ?? null : null,
runnerDisposition: auth.ok === false ? auth.runnerDisposition ?? null : null,
tokenPresent: token.present === true,
tokenSource: typeof token.source === "string" ? token.source : null,
ghFallbackAttempted: token.ghFallbackAttempted === true,
ghBinaryFound: gh.binaryFound === true,
restApi: compactProbeOk(probes.restApi),
repoVisible: compactProbeOk(probes.repo),
issueRead: compactProbeOk(probes.issueRead),
valuesPrinted: false,
};
}
function prPreflightPolicy(repo: string, number: number): Record<string, unknown> {
return {
readOnly: true,
writesRemote: false,
createsPr: false,
comments: false,
mergesPr: false,
mergeCommandSupported: false,
unsupportedMergeCommand: `bun scripts/cli.ts gh pr merge ${number} --repo ${repo}`,
unideskCliMergeSupported: false,
ordinaryRunnerFinalActionAllowed: true,
note: "This preflight only reads GitHub auth, PR metadata, mergeability, and status checks; the UniDesk REST CLI still never merges PRs.",
};
}
function preflightPullRequestSummary(summary: Record<string, unknown>): Record<string, unknown> {
return {
number: summary.number ?? null,
title: summary.title ?? null,
state: summary.state ?? null,
stateDetail: summary.stateDetail ?? null,
draft: summary.draft ?? null,
url: summary.url ?? null,
author: summary.author ?? null,
head: summary.head ?? null,
base: summary.base ?? null,
headRefName: summary.headRefName ?? null,
baseRefName: summary.baseRefName ?? null,
closed: summary.closed ?? null,
merged: summary.merged ?? null,
mergedAt: summary.mergedAt ?? null,
updatedAt: summary.updatedAt ?? null,
bodyOmitted: true,
};
}
function prCloseoutSummary(
pr: Record<string, unknown>,
metadata: Record<string, unknown>,
statusChecks: Record<string, unknown>,
): Record<string, unknown> {
const blockers: string[] = [];
const pending: string[] = [];
const prState = typeof pr.state === "string" ? pr.state.toLowerCase() : "";
if (prState !== "open") blockers.push(`state:${pr.state ?? "unknown"}`);
if (pr.draft === true) blockers.push("draft:true");
const mergeable = typeof metadata.mergeable === "string" ? metadata.mergeable.toUpperCase() : null;
if (mergeable === "MERGEABLE") {
// Ready signal, combined with mergeStateStatus and checks below.
} else if (mergeable === "CONFLICTING" || mergeable === "NOT_MERGEABLE") {
blockers.push(`mergeable:${mergeable}`);
} else {
pending.push(`mergeable:${mergeable ?? "unknown"}`);
}
const mergeStateStatus = typeof metadata.mergeStateStatus === "string" ? metadata.mergeStateStatus.toUpperCase() : null;
if (mergeStateStatus === "CLEAN") {
// Ready signal.
} else if (mergeStateStatus === "UNKNOWN" || mergeStateStatus === null) {
pending.push(`mergeStateStatus:${mergeStateStatus ?? "unknown"}`);
} else {
blockers.push(`mergeStateStatus:${mergeStateStatus}`);
}
const rollupState = typeof statusChecks.state === "string" ? statusChecks.state.toUpperCase() : null;
const totalContexts = typeof statusChecks.totalContexts === "number" ? statusChecks.totalContexts : 0;
const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {};
const failureCount = Number(counts.failure ?? 0) + Number(counts.cancelled ?? 0) + Number(counts["timed-out"] ?? 0);
const pendingCount = Number(counts.pending ?? 0) + Number(counts.unknown ?? 0);
if (failureCount > 0) blockers.push(`statusChecks:failure:${failureCount}`);
if (pendingCount > 0) pending.push(`statusChecks:pending:${pendingCount}`);
if (rollupState === "SUCCESS" || rollupState === null && totalContexts === 0) {
// Ready signal.
} else if (rollupState === "PENDING" || rollupState === "EXPECTED" || rollupState === null) {
pending.push(`statusCheckRollup:${rollupState ?? "unknown"}`);
} else if (rollupState !== null) {
blockers.push(`statusCheckRollup:${rollupState}`);
}
const readyForCommanderMerge = blockers.length === 0 && pending.length === 0;
return {
readyForCommanderMerge,
conclusion: readyForCommanderMerge ? "ready" : blockers.length > 0 ? "blocked" : "pending",
blockers,
pending,
commanderAction: readyForCommanderMerge
? "review and merge through a repo-owned GitHub path when task boundaries allow; UniDesk REST gh pr merge remains unsupported"
: "resolve blockers or rerun after GitHub finishes computing mergeability/status checks",
};
}
async function prPreflight(repo: string, number: number, commandName: "preflight" | "pr preflight" | "pr closeout", includeRaw: boolean): Promise<GitHubCommandResult> {
const auth = await authStatus(repo);
const authCapability = compactAuthCapability(auth);
const policy = prPreflightPolicy(repo, number);
if (auth.ok === false) {
return {
ok: false,
command: commandName,
repo,
number,
degradedReason: auth.degradedReason,
runnerDisposition: auth.runnerDisposition,
authCapability,
policy,
details: auth,
};
}
const { token, probe } = resolveToken(true);
const missing = authRequired(repo, commandName, probe);
if (missing !== null || token === null) {
const missingResult = missing ?? authRequired(repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return {
...(missingResult ?? commandError(commandName, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required"))),
command: commandName,
number,
authCapability,
policy,
} as GitHubCommandResult;
}
const { owner, name } = repoParts(repo);
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number, authCapability, policy });
const metadata = await prGraphqlMetadata(repo, token, number);
const summary = prSummary(pr);
const pullRequest = preflightPullRequestSummary(summary);
if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest, authCapability, policy });
const metadataSummary = prMetadataSummary(metadata);
const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, includeRaw);
const closeout = prCloseoutSummary(summary, metadataSummary, statusChecks);
return {
ok: true,
command: commandName,
canonicalCommand: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo}`,
aliases: ["bun scripts/cli.ts gh preflight <number>", "bun scripts/cli.ts gh pr closeout <number>"],
repo,
number,
readOnly: true,
writesRemote: false,
valuesPrinted: false,
authCapability,
pullRequest,
mergeability: {
mergeable: metadataSummary.mergeable,
mergeStateStatus: metadataSummary.mergeStateStatus,
...closeout,
},
statusChecks,
policy,
...(includeRaw ? { raw: { authStatus: auth, pullRequest, closeoutMetadata: metadataSummary } } : {}),
};
}
function repoSummary(repo: GitHubRepository): Record<string, unknown> {
return {
id: repo.id ?? null,
@@ -5238,11 +5496,14 @@ export function ghHelp(): unknown {
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
"bun scripts/cli.ts gh preflight <prNumber> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
"bun scripts/cli.ts gh pr list [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]",
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [compatibility alias for pr files; no raw diff]",
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw]",
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [compatibility alias for pr preflight]",
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--dry-run]",
@@ -5279,6 +5540,7 @@ export function ghHelp(): unknown {
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure.",
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
"PR create/edit/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
],
};
@@ -5313,14 +5575,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
return validationError(command, options.repo, "--json field selection is only supported by gh issue read/view/list and gh pr read/view/list");
}
}
if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && isIssueReadCommand(sub)) || (top === "pr" && isPrReadCommand(sub)))) {
if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && isIssueReadCommand(sub)) || top === "preflight" || (top === "pr" && (isPrReadCommand(sub) || sub === "preflight" || sub === "closeout")))) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue read/view and gh pr read/view.", {
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue read/view, gh pr read/view, and gh pr preflight/closeout.", {
supportedCommands: [
"bun scripts/cli.ts gh issue read owner/name#<number> --raw",
"bun scripts/cli.ts gh issue read <number> --repo owner/name --json body,title,state,comments",
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
`bun scripts/cli.ts gh pr read <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
"bun scripts/cli.ts gh pr preflight <number> --repo owner/name --full",
],
});
}
@@ -5380,6 +5643,12 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (top === "auth" && sub === "status") return authStatus(options.repo);
if (top === "preflight") {
const number = parseNumberForCommand(options.repo, sub, "preflight");
if (typeof number !== "number") return number;
return prPreflight(options.repo, number, "preflight", options.full || options.raw);
}
if (top === "issue") {
if (sub === "delete") return unsupportedCommand("issue delete", options.repo, "GitHub REST does not support hard-deleting issues; use gh issue close for lifecycle deletion semantics.");
if (sub === "comment" && third === "delete") {
@@ -5498,6 +5767,11 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
return prFiles(options.repo, token, number, options.limit, "pr files");
}
if (sub === "delete") return unsupportedCommand("pr delete", options.repo, "GitHub REST does not support hard-deleting pull requests; use gh pr close for lifecycle deletion semantics.");
if (sub === "preflight" || sub === "closeout") {
const number = parseNumberForCommand(options.repo, third, `pr ${sub}`);
if (typeof number !== "number") return number;
return prPreflight(options.repo, number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
}
if (sub === "comment" && third === "delete") {
const commentId = parseNumberForCommand(options.repo, args[3], "pr comment delete");
if (typeof commentId !== "number") return commentId;
@@ -5573,7 +5847,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
);
}
if (sub !== "list" && !isPrReadCommand(sub)) {
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, files, diff --stat, read/view, create, update/edit, close, reopen, comment create/delete, and unsupported merge/delete.");
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, files, diff --stat, read/view, preflight/closeout, create, update/edit, close, reopen, comment create/delete, and unsupported merge/delete.");
}
if (sub === "read" || sub === "view") {
const resolved = resolveReadViewNumberReference("pr", sub, third, options, args);
+1 -1
View File
@@ -46,7 +46,7 @@ export function rootHelp(): unknown {
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and merge blocked." },
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },